I'm trying to test a React component with some propType logic. The component should take a myprop prop that should match {value: [anything]}.
The first test works, but the second (which is exactly the same code) doesn't.
Any idea why ?
EDIT : I think it's React that does not call console.error() every time, but only once. Probably to avoid spamming the console...
import React from 'react';
import chai, { expect } from 'chai';
import chaiEnzyme from 'chai-enzyme'
import { shallow } from 'enzyme';
chai.use(chaiEnzyme());
class MyComponent extends React.Component {
render() {
return (
<div>
{this.props.myprop.value}
</div>
);
}
}
MyComponent.propTypes = {
myprop: (props, propName, componentName) => {
const prop = props[propName];
if (typeof(prop) !== 'object') {
return new Error(`<${componentName}> requires the prop \`${propName}\``);
}
else {
if (typeof(prop.error) === 'undefined' || !prop.error) {
if (typeof(prop.value) === 'undefined') {
return new Error(`<${componentName}> requires the prop \`${propName}\` to have a \`value\` field`);
}
}
}
return null;
},
};
//Mocking console.error to throw propType errors
let console_error = console.error;
console.error = function(warning) {
if (/(Invalid prop|Failed prop type)/.test(warning)) {
throw new Error(warning);
}
console_error.apply(console, arguments);
};
describe('<MyComponent/>', function() {
describe('with invalid `myprop` format', function() {
it('should throw an error', function(done) {
//This test passes
//Execute
const fn = () => shallow(<MyComponent myprop={{}}/>);
//Verify
expect(fn).to.throw(Error, 'requires the prop \`myprop\` to have a \`value\` field');
done();
});
it('should throw an error', function(done) {
//Exactly the same code as above -- does not pass
//Execute
const fn = () => shallow(<MyComponent myprop={{}}/>);
//Verify
expect(fn).to.throw(Error, 'requires the prop \`myprop\` to have a \`value\` field');
done();
});
});
});
Related
For example if TestFailComponent tries to access item.id, but item is null:
describe("Should Fail Tests", () => {
describe("render tests", () => {
it("should fail to render since null has no id", () => {
let shouldFail = mount(
<MemoryRouter>
<TestFailComponent
item={null} />
</MemoryRouter>
);
chai.expect(shouldFail.render).to.throw(new TypeError('null has no properties'))
});
});
});
I've also tried:
chai.expect(shouldFail.exists()).to.be.false()
chai.expect(shouldFail.render).to.be.null()
related:
Test whether React component has rendered
You can create error boundary component to test it.
Complete file, which works is:
import React from 'react';
import { mount } from 'enzyme';
// assume this component we want to test
const TestFailComponent = ({ items }) => {
if (items == null) {
throw new TypeError('null has no properties');
}
return 'hello world';
};
// We create a component-helper
class ErrorCatching extends React.Component {
state = { hasError: false };
// we need to change state, so that render cycle could be completed
static getDerivedStateFromError() {
return { hasError: true };
}
// catch error and test it
componentDidCatch(error, errInfo) {
// we even can check componentStack field into errInfo arg
this.props.processError(error, errInfo);
}
render() {
// after the first error we render nothing
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
describe('Should Fail Tests', () => {
describe('render tests', () => {
it('should fail to render since null has no id', () => {
// our expected error
const expectedError = new TypeError('null has no properties');
mount(
<ErrorCatching processError={(error) => expect(error).toEqual(expectedError)}>
<TestFailComponent item={null} />
</ErrorCatching>
);
});
});
});
You also can check the result of the render which will be null.
More about error handling:
https://reactjs.org/docs/react-component.html#error-boundaries and https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html#targetText=Error%20boundaries%20are%20React%20components,the%20whole%20tree%20below%20them.
I have parent component with state which includes array of objects. I am passing this array to Child component as props. In child component array is sorted then map and as result returns grandchild component. It is working but from test case i received error "TypeError: Cannot read property 'sort' of undefined"
I am trying to check in child component if array exist and if array isn't undefined but i have the same error.
Parent component (airdropDB.airdrops is reference to array of objects from another file)
class ContentPanel extends Component {
constructor(props) {
super(props);
this.state = {
airdrops: airdropDB.airdrops
};
}
render() {
return (
<div>
<FiltrPanel />
<AirdropPanel airdrops={this.state.airdrops}/>
</div>
)
}
}
Child Component
class AirdropPanel extends Component {
render() {
let airdropBlocks = this.props.airdrops.sort((a, b) => {
return b.addDate - a.addDate;
}).map((e, i) => {
return (<Airdrop key={e.title + i}
title={e.title}
value={e.value}
status={e.status}
logo={e.logo} />)
});
return (
<div data-testid="airdropPanel">
{airdropBlocks}
</div>
);
};
};
Result is ok, i have proper and sorted airdropblocks but error from test case is alarming.
Test file:
import React from 'react';
import AirdropPanel from './AirdropPanel';
import { render } from 'react-testing-library';
describe('Airdrop Panel has', () => {
it('Airdrop block inside', () => {
const { getByText } = render(<AirdropPanel />);
expect(getByText(/rozpocznij/i)).toBeInTheDocument();
});
it('multi airdrop blocks inside', () => {
const { getByTestId } = render(<AirdropPanel />);
let moreThanOne = getByTestId("airdropPanel").childElementCount > 1;
expect(moreThanOne).toBe(true);
});
it('airdropBlock dates from newer to older sort function works', () => {
//Test zależny od bazy airdrop
const { getByTestId } = render(<AirdropPanel />);
const firstAirdropBLock = getByTestId("airdropPanel").firstChild;
expect(firstAirdropBLock.firstChild.textContent).toBe("Crypto Circle X");
});
});
.sort isn't run. That is most likely because this.props.airdrops is undefined
I don't know, but are you sure you shouldn't put something in your airdrops prop test?
const { getByText } = render(<AirdropPanel airdrops={??}/>);
Test is updated. Able to do some other tests using the same format.
This is what I have so far
headers.test.js
import React, {Component} from 'react';
import {Headers} from './Headers';
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-15';
import renderer from 'react-test-renderer';
configure({adapter: new Adapter()});
describe('Headers', () => {
let tree;
let baseProps;
let mockauthKeyValues;
let mockheaderKeyValues;
let mockaddNewKeyValue;
beforeEach(() => {
baseProps = { // assing all of the props into MOCK
authKeyValues: mockauthKeyValues,
headerKeyValues: mockheaderKeyValues,
addNewKeyValue: mockaddNewKeyValue
}
})
it('should render with all of the props', () => {
tree = renderer.create(<Headers {...baseProps} />)
let treeJson = tree.toJSON()
expect(treeJson).toMatchSnapshot();
tree.unmount()
});
});
I will be testing each props and make sure it renders without it.
I believe I am making at mistake when I mock all of this.props. Especially mockheaderKeyValues. I dont think i am setting up correctly.
This is the current code: headers.js
export class Headers extends Component {
componentDidMount () {
if (this.props.authKeyValues == null) {
this.setState({
useDefaultData: false
})
} else {
this.setState({
useDefaultData: true
})
}
}
generateKeyValues = () => {
if (this.props.headerKeyValues == null) {
return (
<KeyValue
id={shortid.generate()}
type='headers'
addNewKeyValue={this.props.addNewKeyValue}
/>
)
} else {
let defaultKeyValues = Object.keys(this.props.headerKeyValues).map ((headerKey, idx) => {
return (
<KeyValue
key={headerKey}
id={headerKey}
type={this.props.headerKeyValues[headerKey].type}
addNewKeyValue={this.props.addNewKeyValue}
defaultData={this.props.headerKeyValues[headerKey]}
/>
)
})
defaultKeyValues.push(
<KeyValue
id={shortid.generate()}
type='headers'
addNewKeyValue={this.props.addNewKeyValue}
/>
)
return defaultKeyValues
}
}
render () {
return (
<div>
{this.generateKeyValues()}
</div>
)
}
}
Found the problem -
Had to remove {} from import {Headers} since Headers import 2 more files , therefore it was not importing correctly
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);
});
});
})
Is there a way I can kill/(get rid of) a timeout in reactjs?
setTimeout(function() {
//do something
}.bind(this), 3000);
Upon some sort of click or action, I want to be able to completely stop and end the timeout. Is there a way to do this? thanks.
Assuming this is happening inside a component, store the timeout id so it can be cancelled later. Otherwise, you'll need to store the id somewhere else it can be accessed from later, like an external store object.
this.timeout = setTimeout(function() {
// Do something
this.timeout = null
}.bind(this), 3000)
// ...elsewhere...
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null
}
You'll probably also want to make sure any pending timeout gets cancelled in componentWillUnmount() too:
componentWillUnmount: function() {
if (this.timeout) {
clearTimeout(this.timeout)
}
}
If you have some UI which depends on whether or not a timeout is pending, you'll want to store the id in the appropriate component's state instead.
Since React mixins are now deprecated, here's an example of a higher order component that wraps another component to give the same functionality as described in the accepted answer. It neatly cleans up any remaining timeouts on unmount, and gives the child component an API to manage this via props.
This uses ES6 classes and component composition which is the recommended way to replace mixins in 2017.
In Timeout.jsx
import React, { Component } from 'react';
const Timeout = Composition => class _Timeout extends Component {
constructor(props) {
super(props);
}
componentWillMount () {
this.timeouts = [];
}
setTimeout () {
this.timeouts.push(setTimeout.apply(null, arguments));
}
clearTimeouts () {
this.timeouts.forEach(clearTimeout);
}
componentWillUnmount () {
this.clearTimeouts();
}
render () {
const { timeouts, setTimeout, clearTimeouts } = this;
return <Composition
timeouts={timeouts}
setTimeout={setTimeout}
clearTimeouts={clearTimeouts}
{ ...this.props } />
}
}
export default Timeout;
In MyComponent.jsx
import React, { Component } from 'react';
import Timeout from './Timeout';
class MyComponent extends Component {
constructor(props) {
super(props)
}
componentDidMount () {
// You can access methods of Timeout as they
// were passed down as props.
this.props.setTimeout(() => {
console.log("Hey! I'm timing out!")
}, 1000)
}
render () {
return <span>Hello, world!</span>
}
}
// Pass your component to Timeout to create the magic.
export default Timeout(MyComponent);
You should use mixins:
// file: mixins/settimeout.js:
var SetTimeoutMixin = {
componentWillMount: function() {
this.timeouts = [];
},
setTimeout: function() {
this.timeouts.push(setTimeout.apply(null, arguments));
},
clearTimeouts: function() {
this.timeouts.forEach(clearTimeout);
},
componentWillUnmount: function() {
this.clearTimeouts();
}
};
export default SetTimeoutMixin;
...and in your component:
// sampleComponent.js:
import SetTimeoutMixin from 'mixins/settimeout';
var SampleComponent = React.createClass({
//mixins:
mixins: [SetTimeoutMixin],
// sample usage
componentWillReceiveProps: function(newProps) {
if (newProps.myValue != this.props.myValue) {
this.clearTimeouts();
this.setTimeout(function(){ console.log('do something'); }, 2000);
}
},
}
export default SampleComponent;
More info: https://facebook.github.io/react/docs/reusable-components.html
I stopped a setTimeout in my react app with Javascript only:
(my use case was to auto-save only after a clear 3 seconds of no keystrokes)
timeout;
handleUpdate(input:any) {
this.setState({ title: input.value }, () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.saveChanges(), 3000);
});
}