React-redux infinite remounting component - reactjs

I'm beginner in react/redux and I came across a weird behaviour of my application
every try of dispatching action e.g.
store.dispatch({type: 'no_matter_what_is_here'});
remounting all components even if the state of store doesn't changed over and over and provides infinite rendering of component (component using 'connect' function from 'react-redux' library).
I'm using these libraries:
"dependencies": {
"babel-polyfill": "^6.3.14",
"bluebird": "^3.4.1",
"eventsource-polyfill": "^0.9.6",
"font-awesome-webpack": "0.0.4",
"history": "^4.7.2",
"lodash": "^4.17.4",
"material-ui": "^0.19.1",
"moment": "^2.13.0",
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-dropzone": "^3.5.1",
"react-modal": "^1.4.0",
"react-redux": "^5.0.2",
"react-router": "^3.0.0",
"react-router-dom": "^4.2.2",
"react-router-redux": "^4.0.6",
"react-scripts": "1.0.13",
"react-tap-event-plugin": "^2.0.1",
"redux": "^3.6.0",
"superagent": "^3.1.0",
"uuid": "^3.0.1"
},
What is the cause of this behaviour?
Code of example component(but it's concern every components in application)
import React, { Component } from 'react';
import store from '../../store';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import _ from 'lodash';
import { CircularProgress } from 'material-ui';
import debounce from '../../modules/debounce';
import { JobAddDialog } from '../job';
import WorkdeckPanel from './workdeckPanel';
import QueuedJobList from './queuedJobList';
import { Label, WarningDialog, InfoDialog } from '../controls';
import { setupActions, jobActions } from '../../actions';
import {
connectTask,
reConnectTask,
cancelTask,
loadStatus,
startTask,
pauseTask,
stopTask,
skipTask,
retryTask,
} from '../../actions/printerActions';
import { getEnumName } from '../../modules/enumHelpers';
import { printerErrorTypeEnum, printerStatusEnum } from '../../constants';
class Status extends Component {
static get propTypes() {
return {
printer: PropTypes.shape({
status: PropTypes.number.isRequired,
}),
jobs: PropTypes.shape({
list: PropTypes.array.isRequired,
}).isRequired,
resources: PropTypes.shape({}).isRequired,
};
}
static get defaultProps() {
return {
printer: {},
jobs: {
list: [],
},
};
}
constructor(props) {
super(props);
this.state = {
showAddDialog: false,
showConfirm: false,
showStop: false,
selectedJobId: null,
};
this.onJobSelected = this.onJobSelected.bind(this);
this.onStatusLoaded = this.onStatusLoaded.bind(this);
}
componentWillMount() {
store.dispatch({type: 'no_matter_what'});
}
componentDidUpdate() {
const { printer } = this.props;
const { showAddDialog } = this.state;
const { isLoading, status } = (printer || {});
if (!isLoading && !showAddDialog
&& [printerStatusEnum.notResponding, printerStatusEnum.confirming, printerStatusEnum.suspended].indexOf(status) === -1) {
debounce(this.onStatusLoaded, 1000);
}
}
onStatusLoaded() {
const { jobs, printer } = this.props;
loadStatus(printer.updateDate)
.then((res) => {
const job = Object.assign({ id: -1 }, printer.job);
const newJob = res.job ? _.find(jobs.list, { id: res.job.id }) : { id: -1 };
if (newJob.id !== job.id) {
return jobActions.loadJobs();
}
return null;
});
}
onJobSelected(selectedJobId) {
this.setState({ selectedJobId });
}
render() {
const { jobs, printer, resources } = this.props;
const { selectedJobId, showAddDialog, showConfirm, showStop } = this.state;
return (
<div className="statusContainer">
<QueuedJobList {...{
jobs, selectedJobId, onJobSelect: this.onJobSelected, onJobAdd: () => { this.setState({ showAddDialog: true }); },
}}
/>
<WorkdeckPanel {...{ jobs,
printer,
selectedJobId,
resources,
onStartClick: () => {
if (printer.job && printer.status === printerStatusEnum.suspended) {
this.setState({ showConfirm: true });
} else {
startTask();
}
},
onStopClick: () => { this.setState({ showStop: true }); },
}}
/>
{ [printerStatusEnum.initializing].indexOf(printer.status) !== -1
? (<InfoDialog {...{
title: resources.get('device.connecting.title'),
show: true,
onCancel: cancelTask }}
>
<div className="iconProgress"><CircularProgress thickness={5} /></div>
<h3 className="textAlert">
<Label path="device.connecting.note" />
</h3>
</InfoDialog>)
: null }
<JobAddDialog {...{
show: showAddDialog,
isQueued: true,
onClose: () => { this.setState({ showAddDialog: false }); },
}}
/>
<ConfirmDialog {...{ printer, showConfirm, onHide: () => { this.setState({ showConfirm: false }); }, resources }} />
<NotRespondingDialog {...this.props} />
<ErrorDialog {...{ showStop, onCancel: () => { this.setState({ showStop: true }); }, printer, resources }} />
<StopDialog {...{ show: showStop, onClose: () => { this.setState({ showStop: false }); }, resources }} />
</div>
);
}
}
const ConfirmDialog = ({ printer, showConfirm, onHide, resources }) => {
const { status, method } = printer;
let onCancel = onHide;
let show = showConfirm;
if (status === printerStatusEnum.confirming) {
onCancel = () => {
onHide();
cancelTask();
};
show = true;
}
if (show) {
return (
<InfoDialog {...{
title: resources.get('device.confirming.title'),
okCaption: resources.get('buttons.continue'),
show,
onCancel,
onOk: () => {
onHide();
startTask();
},
}}
>
<Label {...{ path: 'device.confirming.note', replacements: method }} />
</InfoDialog>);
}
return null;
};
const NotRespondingDialog = (props) => {
const { resources, printer } = props;
if (printer.status === printerStatusEnum.notResponding) {
return (
<WarningDialog {...{
title: resources.get('device.notResponding.title'),
okCaption: resources.get('buttons.retry'),
show: true,
buttons: [
{ type: 'Cancel', onClick: cancelTask },
{ type: 'Retry', onClick: reConnectTask, isPrimary: true },
] }}
>
<Label path="device.notResponding.note" />
</WarningDialog>);
}
return null;
};
const ErrorDialog = ({ showStop, onCancel, printer, resources }) => {
const { status, errorType } = printer;
if (status === printerStatusEnum.inError && !showStop) {
const error = getEnumName(printerErrorTypeEnum, errorType);
let buttons = [
{ type: 'Ok', onClick: pauseTask, isPrimary: true },
];
if (errorType === printerErrorTypeEnum.tubeError) {
buttons = [
{ type: 'Cancel', onClick: onCancel },
{ type: 'Skip', onClick: skipTask },
{ type: 'Retry', onClick: retryTask, isPrimary: true },
];
}
return (
<WarningDialog {...{
title: resources.get(`device.${error}.title`),
show: true,
buttons }}
>
<Label {...{ path: `device.${error}.note` }} />
</WarningDialog>);
}
return null;
};
const StopDialog = ({ show, onClose, resources }) => {
if (show) {
return (
<WarningDialog {...{
title: resources.get('device.stopping.title'),
show: true,
buttons: [
{ type: 'Cancel', onClick: onClose },
{ type: 'Ok', onClick: () => { stopTask().then(() => { onClose(); }); }, isPrimary: true },
] }}
>
<Label className="textInfo" path="device.stopping.note" />
</WarningDialog>);
}
return null;
};
export default connect(
state => ({
jobs: state.jobs,
printer: state.printer,
resources: state.resources,
}),
)(Status);

First of all you should split this code into Presentational Componentes and Containers. Containers keep the logic and the connect with your store. This would make your code less error prone and easier to read.
Regarding your issue you are make this this dispatch store.dispatch({type: 'no_matter_what'}); on componentWillMount. It isn't a good practise as you can read here. I'd recommend you to remove it from there.
Also, I'd look into those bindings you have there. Try to understand if you really need them as they are atm. I don't have enough knowledge but I'd look into this article which is pretty nice (not perfect though).
In this case, I suggest you to use arrow functions and make sure you need this onStatusLoaded binding. You are binding it in the constructor and inside onStatusLoaded you seem to be updating status again every time your component updates, which will cause a cycle.

Your mistake likely in componentDidUpdate method, you need to console each step all values in conditions to catch permament reconcollation:

Related

Adding a Modal Instead of "prompt" in react app

Adding a Modal Instead of "prompt" in react app
Hi Iam creating a menu builder with react-sortable-tree. I want to add a Modal when clicked on Add or Edit Task. prompt is working fine here but i want Modal to be opened. I have created state and finctions for modal but unable to render on UI when clicked. anybody help
import React, { Component } from "react";
import "bootstrap/dist/css/bootstrap.css";
import "react-sortable-tree/style.css";
import { Button } from "react-bootstrap";
import "./MenuBuilder.css";
import { Modal } from "react-responsive-modal";
import "react-responsive-modal/styles.css";
import treeData from "./MenuBuilderData";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faTrashAlt, faPlus, faPen } from "#fortawesome/free-solid-svg-icons";
import SortableTree, {
toggleExpandedForAll,
getNodeAtPath,
addNodeUnderParent,
removeNode,
changeNodeAtPath,
} from "react-sortable-tree";
const maxDepth = 5;
export default class MenuBuilder extends Component {
constructor(props) {
super(props);
this.state = {
treeData: treeData,
searchString: "",
searchFocusIndex: 0,
searchFoundCount: null,
openModal: false,
};
}
onOpenModal = (e) => {
e.preventDefault();
this.setState({ openModal: true });
};
onCloseModal = () => {
this.setState({ openModal: false });
};
handleTreeOnChange = (treeData) => {
this.setState({ treeData });
};
selectPrevMatch = () => {
const { searchFocusIndex, searchFoundCount } = this.state;
this.setState({
searchFocusIndex:
searchFocusIndex !== null
? (searchFoundCount + searchFocusIndex - 1) % searchFoundCount
: searchFoundCount - 1,
});
};
selectNextMatch = () => {
const { searchFocusIndex, searchFoundCount } = this.state;
this.setState({
searchFocusIndex:
searchFocusIndex !== null
? (searchFocusIndex + 1) % searchFoundCount
: 0,
});
};
toggleNodeExpansion = (expanded) => {
this.setState((prevState) => ({
treeData: toggleExpandedForAll({
treeData: prevState.treeData,
expanded,
}),
}));
};
getNodeKey = ({ treeIndex: number }) => {
if (number === -1) {
number = null;
}
return number;
};
handleSave = () => {
console.log(JSON.stringify(this.state.treeData));
};
editTask = (path) => {
let editedNode = getNodeAtPath({
treeData: this.state.treeData,
path: path,
getNodeKey: ({ treeIndex }) => treeIndex,
ignoreCollapsed: true,
});
let newTaskTitle = prompt("Task new name:", editedNode.node.title);
if (newTaskTitle === null) return false;
editedNode.node.title = newTaskTitle;
let newTree = changeNodeAtPath({
treeData: this.state.treeData,
path: path,
newNode: editedNode.node,
getNodeKey: ({ treeIndex }) => treeIndex,
ignoreCollapsed: true,
});
// console.log(newTree);
this.setState({ treeData: newTree });
};
addTask = (path) => {
let parentNode = getNodeAtPath({
treeData: this.state.treeData,
path: path,
getNodeKey: ({ treeIndex }) => treeIndex,
ignoreCollapsed: true,
});
let newTaskTitle = parentNode.node.children ? prompt("Task name:", "default") && prompt("form ID:", "default") : prompt("Task name:", "default") // let newFormId = prompt("Form Id:", "");
if (newTaskTitle === null) return false;
let NEW_NODE = { title: newTaskTitle };
// let NEW_ID = { id: newFormId };
let parentKey = this.getNodeKey(parentNode);
let newTree = addNodeUnderParent({
treeData: this.state.treeData,
newNode: NEW_NODE,
// newId: NEW_ID,
expandParent: true,
parentKey: parentKey,
getNodeKey: ({ treeIndex }) => treeIndex,
});
this.setState({ treeData: newTree.treeData });
};
removeTask = (path) => {
let newTree = removeNode({
treeData: this.state.treeData,
path: path,
ignoreCollapsed: true,
getNodeKey: ({ treeIndex }) => treeIndex,
});
this.setState({ treeData: newTree.treeData });
};
renderTasks = () => {
const { treeData, searchString, searchFocusIndex } = this.state;
return (
<>
<SortableTree
treeData={treeData}
onChange={this.handleTreeOnChange}
maxDepth={maxDepth}
searchQuery={searchString}
searchFocusOffset={searchFocusIndex}
canDrag={({ node }) => !node.noDragging}
canDrop={({ nextParent }) => !nextParent || !nextParent.noChildren}
searchFinishCallback={(matches) =>
this.setState({
searchFoundCount: matches.length,
searchFocusIndex:
matches.length > 0 ? searchFocusIndex % matches.length : 0,
})
}
isVirtualized={true}
generateNodeProps={(taskInfo) => ({
buttons: [
<Button
variant="link"
onClick={() => this.editTask(taskInfo.path)}
>
<FontAwesomeIcon icon={faPen} color="#28a745" />
</Button>,
<Button
variant="link"
onClick={() => this.addTask(taskInfo.path)}
>
<FontAwesomeIcon icon={faPlus} color="#007bff" />
</Button>,
<Button
variant="link"
onClick={() => this.removeTask(taskInfo.path)}
>
<FontAwesomeIcon icon={faTrashAlt} color="#dc3545" />
</Button>,
],
})}
/>
<Button style={{ width: "100px" }} onClick={this.handleSave}>
save
</Button>
</>
);
};
render() {
return (
<>
<div className="wrapper">{this.renderTasks()}</div>
{/* <div>
<button onClick={this.onOpenModal}>Click Me</button>
<Modal open={this.state.openModal} onClose={this.onCloseModal}>
<input type="text" />
</Modal>
</div> */}
</>
);
}
}
treeData.js
const treeData = [
{
expanded: true,
title: "Contact HR",
children: [
{
expanded: true,
title: "Build relationships"
},
{
expanded: true,
title: "Take a test assignment"
}
]
},
{
expanded: true,
title: "Complete a test assignment",
children: [
{
expanded: true,
title: "Send link to this doc through LinkedIn"
}
]
},
{
expanded: true,
title: "Discuss Proposal details",
children: [
{
expanded: true,
title: "Prepare list of questions."
},
{
expanded: true,
title: "Other coming soon..."
}
]
},
{
expanded: true,
title: "Make an appointment for a technical interview",
children: [
{
expanded: true,
title: "Discuss details of the technical interview"
},
{
expanded: true,
title: "Prepare to Technival Interview"
}
]
},
{
expanded: true,
title: "Accept or Decline Offer"
}
];
export default treeData;

Type 'Function' is not assignable to type '(editorState: EditorState) => void'

I am building rich text editor using react with typescript But I am getting error which I have attached in the snapshot and also I have pasted my EditorState.tsx code
So could you please let me know why I am getting this error
import React, {Component} from 'react';
import { render } from 'react-dom';
import {EditorState} from "draft-js";
import {Editor} from "react-draft-wysiwyg";
type MyProps = {
}
type MyState = {
editorState:any
}
class EditorContainer extends Component<MyProps,MyState>{
constructor(props:MyProps){
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
}
onEditorStateChange: Function = (editorState:any) => {
// console.log(editorState)
this.setState({
editorState,
});
};
render(){
const { editorState } = this.state;
return <div className='editor'>
<Editor
editorState={editorState}
onEditorStateChange={this.onEditorStateChange}
toolbar={{
inline: { inDropdown: true },
list: { inDropdown: true },
textAlign: { inDropdown: true },
link: { inDropdown: true },
history: { inDropdown: true },
// image: { uploadCallback: uploadImageCallBack, alt: { present: true, mandatory: true } },
}}
/>
</div>
}
}
onEditorStateChange: Function = (editorState:any) => {
You've given this the type Function, which means it could be literally any function. Then when you try to pass the function into the editor, typescript is pointing out that only specific functions are allowed, not any function.
Instead, do:
onEditorStateChange = (editorState: EditorState) => {
this.setState({
editorState,
});
};

How to test react-dropzone with Jest and react-testing-library?

I want to test onDrop method from react-dropzone library in React component. I am using Jest, React Testing Library. I'm creating mock file and I'm trying to drop this files in input, but in console.log files are still equal to an empty array. Do you have any ideas?
package.json
"typescript": "^3.9.7",
"#testing-library/jest-dom": "^5.11.4",
"#testing-library/react": "^11.0.4",
"#types/jest": "^26.0.13",
"jest": "^26.4.2",
"ts-jest": "^26.3.0",
"react-router-dom": "^5.1.2",
"react-dropzone": "^10.1.10",
"#types/react-dropzone": "4.2.0",
ModalImportFile.tsx
import React, { FC, useState } from "react";
import { Box, Button, Dialog, DialogContent, DialogTitle, Grid } from "#material-ui/core";
import { useDropzone } from "react-dropzone";
import AttachFileIcon from "#material-ui/icons/AttachFile";
import DeleteIcon from "#material-ui/icons/Delete";
interface Props {
isOpen: boolean;
}
interface Events {
onClose: () => void;
}
const ModalImportFile: FC<Props & Events> = props => {
const { isOpen } = props as Props;
const { onClose } = props as Events;
const [files, setFiles] = useState<Array<File>>([]);
const { getRootProps, getInputProps, open } = useDropzone({
onDrop: (acceptedFiles: []) => {
setFiles(
acceptedFiles.map((file: File) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
}),
),
);
},
noClick: true,
noKeyboard: true,
});
const getDragZoneContent = () => {
if (files && files.length > 0)
return (
<Box border={1} borderRadius={5} borderColor={"#cecece"} p={2} mb={2}>
<Grid container alignItems="center" justify="space-between">
<Box color="text.primary">{files[0].name}</Box>
<Box ml={1} color="text.secondary">
<Button
startIcon={<DeleteIcon color="error" />}
onClick={() => {
setFiles([]);
}}
/>
</Box>
</Grid>
</Box>
);
return (
<Box border={1} borderRadius={5} borderColor={"#cecece"} p={2} mb={2} style={{ borderStyle: "dashed" }}>
<Grid container alignItems="center">
<Box mr={1} color="text.secondary">
<AttachFileIcon />
</Box>
<Box color="text.secondary">
<Box onClick={open} component="span" marginLeft="5px">
Download
</Box>
</Box>
</Grid>
</Box>
);
};
const closeHandler = () => {
onClose();
setFiles([]);
};
return (
<Dialog open={isOpen} onClose={closeHandler}>
<Box width={520}>
<DialogTitle>Import</DialogTitle>
<DialogContent>
<div data-testid="container" className="container">
<div data-testid="dropzone" {...getRootProps({ className: "dropzone" })}>
<input data-testid="drop-input" {...getInputProps()} />
{getDragZoneContent()}
</div>
</div>
</DialogContent>
</Box>
</Dialog>
);
};
export default ModalImportFile;
ModalImportFile.test.tsx
import React from "react";
import { render, screen, fireEvent } from "#testing-library/react";
import ModalImportFile from "../../components/task/elements/ModalImportFile";
const props = {
isOpen: true,
onClose: jest.fn(),
};
beforeEach(() => jest.clearAllMocks());
describe("<ModalImportFile/>", () => {
it("should drop", async () => {
render(<ModalImportFile {...props} />);
const file = new File([JSON.stringify({ ping: true })], "ping.json", { type: "application/json" });
const data = mockData([file]);
function dispatchEvt(node: any, type: any, data: any) {
const event = new Event(type, { bubbles: true });
Object.assign(event, data);
fireEvent(node, event);
}
function mockData(files: Array<File>) {
return {
dataTransfer: {
files,
items: files.map(file => ({
kind: "file",
type: file.type,
getAsFile: () => file,
})),
types: ["Files"],
},
};
}
const inputEl = screen.getByTestId("drop-input");
dispatchEvt(inputEl, "dragenter", data);
});
}
With the rokki`s answer (https://stackoverflow.com/a/64643985/9405587), I rewrote the test component for easier understanding.
ModalImportFile.test.tsx
import React from "react";
import { render, screen, fireEvent } from "#testing-library/react";
import ModalImportFile from "../../components/task/elements/ModalImportFile";
const props = {
isOpen: true,
onClose: jest.fn(),
};
beforeEach(() => jest.clearAllMocks());
describe("<ModalImportFile/>", () => {
it("should drop", async () => {
render(<ModalImportFile {...props} />);
window.URL.createObjectURL = jest.fn().mockImplementation(() => "url");
const inputEl = screen.getByTestId("drop-input");
const file = new File(["file"], "ping.json", {
type: "application/json",
});
Object.defineProperty(inputEl, "files", {
value: [file],
});
fireEvent.drop(inputEl);
expect(await screen.findByText("ping.json")).toBeInTheDocument();
}
How about changing fireEvent(node, event); to fireEvent.drop(node, event);.
References:
https://jestjs.io/docs/jest-object#jestrequireactualmodulename
requireActual
Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not.
let dropCallback = null;
let onDragEnterCallback = null;
let onDragLeaveCallback = null;
jest.mock('react-dropzone', () => ({
...jest.requireActual('react-dropzone'),
useDropzone: options => {
dropCallback = options.onDrop;
onDragEnterCallback = options.onDragEnter;
onDragLeaveCallback = options.onDragLeave;
return {
acceptedFiles: [{
path: 'sample4.png'
},
{
path: 'sample3.png'
}
],
fileRejections: [{
file: {
path: 'FileSelector.docx'
},
errors: [{
code: 'file-invalid-type',
message: 'File type must be image/*'
}]
}],
getRootProps: jest.fn(),
getInputProps: jest.fn(),
open: jest.fn()
};
}
}));
it('Should get on drop Function with parameter', async() => {
const accepted = [{
path: 'sample4.png'
},
{
path: 'sample3.png'
},
{
path: 'sample2.png'
}
];
const rejected = [{
file: {
path: 'FileSelector.docx'
},
errors: [{
code: 'file-invalid-type',
message: 'File type must be image/*'
}]
}];
const event = {
bubbles: true,
cancelable: false,
currentTarget: null,
defaultPrevented: true,
eventPhase: 3,
isDefaultPrevented: () => {},
isPropagationStopped: () => {},
isTrusted: true,
target: {
files: {
'0': {
path: 'FileSelector.docx'
},
'1': {
path: 'sample4.png'
},
'2': {
path: 'sample3.png'
},
'3': {
path: 'sample2.png'
}
}
},
timeStamp: 1854316.299999997,
type: 'change'
};
dropCallback(accepted, rejected, event);
onDragEnterCallback();
onDragLeaveCallback();
expect(handleFiles).toHaveBeenCalledTimes(1);
});

How to generate snapshot after all life cycle methods have called in React Jest

snapshot file has created before componentDidMount() is being called. In my situation, I fetch data from server inside the componentDidMount(). Based on the results, I draw the table. But in my test case, it doesn't show those received mock results.
Test file
import React from 'react';
import renderer from 'react-test-renderer';
import { fakeRequestLibrary } from '../../../__mocks__/fakeRequestLibrary';
import ReportAsTableView from '../../../components/reports/common/ReportAsTableView';
const FAKE_RESPONSE = {
dataSets: [
{
metadata: {
columns: [
{
name: "username",
label: "username"
},
{
name: "date_created",
label: "date_created"
}
]
},
rows: [
{
date_created: "2010-04-26T13:25:00.000+0530",
username: "daemon"
},
{
date_created: "2017-06-08T21:37:18.000+0530",
username: "clerk"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "nurse"
},
{
date_created: "2017-07-08T21:37:19.000+0530",
username: "doctor"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "sysadmin"
}
]
}
]
};
describe('<ReportAsTableView /> ', () => {
it('renders correctly with success data received from server', () => {
const params = {
"startDate": "2017-05-05",
"endDate": "2017-10-05"
};
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary('openmrs-fake-server.org', {}, true, FAKE_RESPONSE)} />
);
expect(rendered.toJSON()).toMatchSnapshot();
});
});
Targeted component class
import React, { Component } from 'react';
import { ApiHelper } from '../../../helpers/apiHelper';
import * as ReportConstants from '../../../helpers/ReportConstants';
import ReactDataGrid from 'react-data-grid';
import DataNotFound from './DataNotFound';
import moment from 'moment';
import './ReportAsTableView.css';
class ReportAsTableView extends Component {
constructor(props) {
super();
this.state = {
report: {
definition: {
name: ''
}
},
reportColumnNames: Array(),
reportRowData: Array()
};
this.resolveResponse = this.resolveResponse.bind(this);
this.rowGetter = this.rowGetter.bind(this);
this.init = this.init.bind(this);
}
componentDidMount() {
this.init(this.props.reportParameters);
}
componentWillReceiveProps(nextProps) {
this.init(nextProps.reportParameters);
}
init(params) {
if(this.props.fetchData != null){
//Test Path
this.props.fetchData
.then((response) => {
console.log('>>>>>'+JSON.stringify(response.body));
this.resolveResponse(response.body);
});
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
resolveResponse(data) {
this.setState({ report: data });
this.setState({ reportColumnNames: data.dataSets[0].metadata.columns });
this.setState({ reportRowData: data.dataSets[0].rows });
}
// ... there are some other methods as well
render() {
return (
<div style={{ border: '1px solid black' }}>
{this.getColumns().length > 0 ? (
<ReactDataGrid
columns={this.getColumns()}
rowGetter={this.rowGetter}
rowsCount={this.state.reportRowData.length} />
) : (
<DataNotFound componentName="Report Table"/>
)}
</div>
);
}
}
export default ReportAsTableView;
Snapshot file
// Jest Snapshot v1,
exports[`<ReportAsTableView /> renders correctly with success data received from server 1`] = `
<div
style={
Object {
"border": "1px solid black",
}
}
>
<div
className="NotFoundWrapper"
>
<div
className="attentionSign"
>
<img
src="./warning.png"
width="300"
/>
</div>
<div>
No Data found
<span>
for
Report Table
</span>
</div>
</div>
</div>
`;
Update:
fakeRequestLibrary
import Response from 'http-response-object';
export const fakeRequestLibrary = (requestUrl, requestOptions, shouldPass = true, responseData = null) => {
return new Promise((resolve, reject) => {
if (shouldPass) {
resolve(new Response(200, {}, responseData || { message: `You called ${requestUrl}` }, requestUrl));
} else {
reject(new Response(404, {}, responseData || { message: `The page at ${requestUrl} was not found` }, requestUrl));
}
});
};
Instead of passing an http end point what you can do for fix your problem is changing your init method and passing the data if no data are passed fetch them. Like this
init(params) {
if(this.props.fetchData != null){
this.resolveResponse(this.props.fetchData);
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
Then in your test you will have
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={FAKE_RESPONSE} />
);
expect(rendered.toJSON()).toMatchSnapshot();
This solution works for my own project. It might also work for this question as well, but I haven't tested it. Add an await wait(); statement to wait for the async function in componentDidMount to complete.
const wait = async () => 'foo'; // a dummy function to simulate waiting
describe('<ReportAsTableView /> ', async () => {
it('renders correctly with success data received from server', async () => {
const params = {
startDate: '2017-05-05',
endDate: '2017-10-05',
};
var rendered = renderer.create(
<ReportAsTableView
reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary(
'openmrs-fake-server.org',
{},
true,
FAKE_RESPONSE,
)}
/>,
);
await wait(); // wait for <ReportAsTableView> to finish async data fetch in componentDidMount()
expect(rendered.toJSON()).toMatchSnapshot(); // shall render the component AFTER componentDidMount() is called
});
});

React test vcoverage doesnot fulfill

import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { startWorkflow } from 'wf-dbd-react-ui/es/actions'
import Block from 'wf-dbd-react-ui/es/Block'
import BrowserTitle from 'wf-dbd-react-ui/es/BrowserTitle'
import ScrollToTopOnMount from 'wf-dbd-react-ui/es/ScrollToTopOnMount'
import FormMessages from 'wf-dbd-react-ui/es/FormMessages'
import { globals } from 'wf-dbd-react-ui/es/lib'
import withStrings from 'wf-dbd-react-ui/es/withStrings'
import PanelHeader from './panel-header/PanelHeader'
import AppHubTabs from './app-hub-tabs/AppHubTabs'
import RightRail from '../right-rail/RightRail'
import MessageDisplay from '../common/message-display/MessageDisplay'
import AddPayeeMessage from '../common/add-payee-message/AddPayeeMessage'
import { ADD_PAYEE, SEARCH_PAYEE } from '../workflows/add-payee/constants'
import ChangeFundingAccount from '../common/change-funding-account/ChangeFundingAccount'
import EditPaymentHotTaskModalContainer from '../common/hottask-edit-payment-modal/EditPaymentHotTaskModalContainer'
import DismissReminder from '../common/dismiss-reminder-modal/DismissReminder'
import HistoryTabViewPaymentDetailsModal from '../history-tab/group-history-form/history-tab-hot-task-menu/HistoryTabViewPaymentDetailsModal'
import * as actions from '../../lib/store/hub/actions'
import { getPayeeDetails } from '../../lib/store/payee-details/actions'
import { getPaymentDetails } from '../../lib/store/payment-details/actions'
import { resetPayeeTabFilterOption } from '../../lib/store/payees/actions'
import { closeViewPaymentDetailsModal } from '../../lib/store/history-tab/actions'
import { getHistoricalPayments } from '../../lib/selectors/selectors'
import styles from './AppHub.less'
class AppHub extends React.Component {
componentDidMount() {
const { getPayeeDetails, getPaymentDetails } = this.props
// payee details need to be fetched unconditionally. the cache in saga ensures it is not fetched until required
getPayeeDetails()
getPaymentDetails()
// Fix for for wide page layout issue in ie11 - Problem: ie11 nested flex element issue when window is minimized
Iif (!!window.MSInputMethodContext && !!document.documentMode) { // will be true for only for ie11 browser
document.querySelector('body > div > div > div > div').style.flexBasis = '0%'
}
// end of fix for ie11 wide page layout issue
}
componentWillUnmount() {
const { clearMessages, resetPayeeTabFilterOption } = this.props
clearMessages()
//clearing the selected payees-tab filter option
resetPayeeTabFilterOption()
}
handleTabSelect = activeTabIndexNew => {
const { clearMessages, activeTabIndex, setActiveTabIndex } = this.props
if (activeTabIndexNew !== activeTabIndex) {
setActiveTabIndex(activeTabIndexNew)
clearMessages()
}
}
render() {
const { activeTabIndex,
getString,
successMessage,
errorMessage,
ineligibleAccountMessage,
payees,
noOfHistoricalPayments,
payeesLoaded,
startAddPayeeWorkflow,
shouldShowChangeAccount,
reminderDismissalInProgress,
reminderDueDate,
closeViewPaymentDetailsModal,
isViewPaymentDetailsModalOpen,
viewPaymentData,
showEditPaymentSeriesModal
} = this.props
const panelHeaderId = globals().billpayBusinessUser ? 'app.hub.business.bill.pay.header' : 'app.hub.bill.pay.header'
return (
<Block layout={true} horizontal={true}>
<BrowserTitle title={getString('app.title')} />
<ScrollToTopOnMount />
<Block flex={true} relative={true} className={styles.billPayHub}>
<PanelHeader headerId={panelHeaderId} />
{
successMessage &&
<Block className={styles.message}>
<MessageDisplay messages={successMessage} />
</Block>
}
{
errorMessage &&
<Block className={styles.message}>
<MessageDisplay messages={errorMessage} />
</Block>
}
{
ineligibleAccountMessage && globals().enableDisplayPayeesWithIneligibleAccounts &&
<Block className={styles.message}>
<MessageDisplay messages={ineligibleAccountMessage} focusOnMount={true} />
</Block>
}
<Block className={styles.message}>
<FormMessages formId="makePaymentForm" className="formMessages" />
</Block>
{payeesLoaded && payees.size === 0 && <AddPayeeMessage startAddPayeeWorkflow={startAddPayeeWorkflow} />}
{payeesLoaded && (payees.size > 0 || noOfHistoricalPayments > 0) && (
<AppHubTabs
activeTabIndex={activeTabIndex}
onTabSelect={this.handleTabSelect}
/>
)
}
</Block>
<Block relative={true} styles={styles.rightRailContainer}>
<RightRail />
</Block>
{shouldShowChangeAccount && <ChangeFundingAccount />}
{showEditPaymentSeriesModal && <EditPaymentHotTaskModalContainer />}
{reminderDismissalInProgress && <DismissReminder dueDate={reminderDueDate} />}
{isViewPaymentDetailsModalOpen &&
<HistoryTabViewPaymentDetailsModal
isOpen={isViewPaymentDetailsModalOpen}
closeModal={closeViewPaymentDetailsModal}
viewPaymentData={viewPaymentData}
/>
}
</Block>
)
}
}
AppHub.propTypes = {
activeTabIndex: PropTypes.number.isRequired,
payees: PropTypes.object.isRequired,
noOfHistoricalPayments: PropTypes.number.isRequired,
payeesLoaded: PropTypes.bool,
startAddPayeeWorkflow: PropTypes.func.isRequired,
errorMessage: PropTypes.object,
successMessage: PropTypes.object,
shouldShowChangeAccount: PropTypes.bool,
resetPayeeTabFilterOption: PropTypes.func.isRequired,
getPayeeDetails: PropTypes.func.isRequired,
getPaymentDetails: PropTypes.func.isRequired,
clearMessages: PropTypes.func.isRequired,
setActiveTabIndex: PropTypes.func.isRequired,
getString: PropTypes.func.isRequired,
reminderDismissalInProgress: PropTypes.bool,
reminderDueDate: PropTypes.string,
closeViewPaymentDetailsModal: PropTypes.func.isRequired,
isViewPaymentDetailsModalOpen: PropTypes.bool,
viewPaymentData: PropTypes.object,
showEditPaymentSeriesModal: PropTypes.func.isRequired,
ineligibleAccountMessage: PropTypes.object
}
const mapStateToProps = state => ({
activeTabIndex: state.app.hub.activeHubTab,
successMessage: state.app.hub.successMessage,
errorMessage: state.app.hub.errorMessage,
ineligibleAccountMessage: state.app.hub.ineligibleAccountMessage,
payees: state.app.payees.payees,
noOfHistoricalPayments: getHistoricalPayments(state),
payeesLoaded: state.app.payees.payeesLoaded,
shouldShowChangeAccount: state.app.hub.showChangeAccount,
showEditPaymentSeriesModal: state.app.editPayment.editPaymentModal.isModalOpen,
reminderDismissalInProgress: state.app.hub.reminderDismissalInProgress,
reminderDueDate: state.app.hub.reminderDueDate,
isViewPaymentDetailsModalOpen: state.app.historyTab.isViewPaymentDetailsModalOpen,
viewPaymentData: state.app.historyTab.viewPaymentDetails
})
const mapDispatchToProps = dispatch => ({
setActiveTabIndex: index => dispatch(actions.setHubActiveTab(index)),
clearMessages: () => dispatch(actions.clearMessages()),
startAddPayeeWorkflow: () => dispatch(startWorkflow({ name: ADD_PAYEE, startingView: SEARCH_PAYEE })),
getPayeeDetails: () => dispatch(getPayeeDetails()),
getPaymentDetails: () => dispatch(getPaymentDetails()),
resetPayeeTabFilterOption: () => dispatch(resetPayeeTabFilterOption()),
closeViewPaymentDetailsModal: () => dispatch(closeViewPaymentDetailsModal())
})
export default compose(withStrings, connect(mapStateToProps, mapDispatchToProps))(AppHub)
iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.iamtryingtofindasolutionpleasehelp.
import React from 'react'
import { mount } from 'enzyme'
import MockStoreProvider from 'wf-dbd-react-ui/es/MockStoreProvider'
import Immutable from 'immutable'
import AppHub from '../AppHub'
import AppHubTabs from '../app-hub-tabs/AppHubTabs'
import AddPayeeMessage from '../../common/add-payee-message/AddPayeeMessage'
import PanelHeader from '../panel-header/PanelHeader'
jest.mock('../panel-header/PanelHeader', () => () => 'PanelHeader')
jest.mock('../app-hub-tabs/AppHubTabs', () => () => 'AppHubTabs')
jest.mock('../../common/add-payee-message/AddPayeeMessage', () => () => 'AddPayeeMessage')
jest.mock('../../common/message-display/MessageDisplay', () => () => 'MessageDisplay')
jest.mock('../../right-rail/RightRail', () => () => 'RightRail')
jest.mock('../../history-tab/group-history-form/history-tab-hot-task-menu/HistoryTabViewPaymentDetailsModal', () => () => 'HistoryTabViewPaymentDetailsModal')
describe('AppHub', () => {
let wrapper
describe('when rendered without any payees and payments', () => {
beforeEach(() => {
const getString = jest.fn().mockImplementation(() => 'testString')
const appState = {
hub: {
activeHubTab: 0,
successMessage: { SuccessMessageBean: { globalMessages: [{ level: 'confirm', message: 'message' }] } },
errorMessage: {}
},
payees: {
payees: Immutable.List([]),
enableSubmitBar: false,
payeesLoaded: true
},
paymentAccounts: {
paymentAccounts: Immutable.List([]),
enableSubmitBar: false
},
scheduledPayments: {
paymentsLoaded: false
},
historyTab: {
isViewPaymentDetailsModalOpen: true,
viewPaymentDetails: { key: 'value' },
historicalPayments: {
payments: []
}
},
editPayment: {
editPaymentModal: {
isModalOpen: false
}
}
}
wrapper = mount(
<MockStoreProvider appState={appState}>
<AppHub getString={getString} />
</MockStoreProvider>
)
})
it('should be defined', () => {
expect(AppHub).toBeDefined()
})
it('should render a PanelHeader', () => {
expect(wrapper.find(PanelHeader)).toHaveLength(1)
})
it('should render AddPayeeMessage', () => {
expect(wrapper.find(AddPayeeMessage)).toHaveLength(1)
})
})
describe('when rendered without any payees and historical payments are present', () => {
beforeEach(() => {
const getString = jest.fn().mockImplementation(() => 'testString')
const appState = {
hub: {
activeHubTab: 0,
successMessage: { SuccessMessageBean: { globalMessages: [{ level: 'confirm', message: 'message' }] } },
errorMessage: {}
},
payees: {
payees: Immutable.List([]),
enableSubmitBar: false,
payeesLoaded: true
},
paymentAccounts: {
paymentAccounts: Immutable.List([]),
enableSubmitBar: false
},
scheduledPayments: {
paymentsLoaded: false
},
historyTab: {
isViewPaymentDetailsModalOpen: true,
viewPaymentDetails: { key: 'value' },
historicalPayments: {
payments: [{
mockKey: 'mockValue'
}]
}
},
editPayment: {
editPaymentModal: {
isModalOpen: false
}
}
}
wrapper = mount(
<MockStoreProvider appState={appState}>
<AppHub getString={getString} />
</MockStoreProvider>
)
})
it('should render AppHubTabs', () => {
expect(wrapper.find(AppHubTabs)).toHaveLength(1)
})
it('should render AddPayeeMessage', () => {
expect(wrapper.find(AddPayeeMessage)).toHaveLength(1)
})
})
describe('when rendered with payees and there are no historical payments', () => {
beforeEach(() => {
const getString = jest.fn().mockImplementation(() => 'testString')
const appState = {
hub: {
activeHubTab: 0,
successMessage: { SuccessMessageBean: { globalMessages: [{ level: 'confirm', message: 'message' }] } },
errorMessage: {}
},
payees: {
payees: Immutable.List([{ id: 1 }]),
enableSubmitBar: false,
payeesLoaded: true
},
paymentAccounts: {
paymentAccounts: Immutable.List([]),
enableSubmitBar: false
},
scheduledPayments: {
paymentsLoaded: false
},
historyTab: {
isViewPaymentDetailsModalOpen: true,
viewPaymentDetails: { key: 'value' },
historicalPayments: {
payments: []
}
},
editPayment: {
editPaymentModal: {
isModalOpen: false
}
}
}
wrapper = mount(
<MockStoreProvider appState={appState}>
<AppHub getString={getString} />
</MockStoreProvider>
)
})
it('should render AppHubTabs', () => {
expect(wrapper.find(AppHubTabs)).toHaveLength(1)
})
it('should render AddPayeeMessage', () => {
expect(wrapper.find(AddPayeeMessage)).toHaveLength(0)
})
})
})

Resources