Closed leanne2 closed 5 years ago
Hello @leanne2,
Jest offer a way to test async/callback. In your case you gonna need to use timer-mock. Internally the event manager use setTimeout
.
could you try the following:
jest.useFakeTimers();
it('correctly renders a success notification', () => {
class App extends Component {
notify = () => {
toast("Wow so easy !")
};
render(){
return (
<div>
{this.notify()}
<ToastContainer />
</div>
);
}
}
wrapper = mount(<App />);
jest.runAllTimers();
expect(toJson(wrapper)).to.matchSnapshot();
});
You can even write the test as follow:
jest.useFakeTimers();
it('correctly renders a success notification', () => {
class App extends Component {
render(){
return (
<div>
<ToastContainer />
</div>
);
}
}
wrapper = mount(<App />);
toast("Wow so easy !");
jest.runAllTimers();
expect(toJson(wrapper)).to.matchSnapshot();
});
Tell me if it's helped you.
@fkhadra Thanks for your response. I am not using the Jest framework, I'm using Mocha. But sinon's fakeTimer
seems to be the same util as Jest's fake timers. If I use a fakeTimer, as shown below, the snapshot does correctly generate, but only on a first clean test run. By this i mean, if i run up my tests once then exit, the toast is rendered and the snapshot outputs the full node tree as expected. However, the problem is if i run my tests in watch mode, the first test correctly generates the shapshot, but subsequent re-runs triggered by unrelated code changes causes the test to fail because as before the toast does not get rendered. So this is not a feasible solution on its own. I also noticed that the integer value returned by the call to toast
increments on each test run. This suggests some clean up is not happening even though I am unmounting the component that renders the ToastContainer
at the end of each test. I have used Mocha / Sinon extensively to write tests for code using fakeTimers and have not seen this problem before so don't believe the issue is with the test framework.
Here is the updated test spec which works correctly only on each clean test run:
it('correctly renders a success notification', () => {
const clock = sinon.useFakeTimers();
class App extends Component {
notify = () => {
const t = toast("Wow so easy !");
console.log('######## t', t); // Increments on each test run: 1, 2, 3, so some cleanup is needed
};
render(){
return (
<div>
{this.notify()}
<ToastContainer />
</div>
);
}
}
wrapper = mount(<App />);
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
clock.restore();
wrapper.unmount();
});
Hello @leanne2 ,
I also noticed that the integer value returned by the call to toast increments on each test run. This suggests some clean up is not happening even though I am unmounting the component that renders the ToastContainer at the end of each test.
You're right. I'll work on fix to to reset the id on umount.
Could you share your package.json with the relevant packages for testing. I want to setup the same test env as yours.
Thanks a lot.
@fkhadra Update: If I set my test up using the second route you suggest then interestingly the snapshot persists correctly between test runs, however the snapshots now fail because, as noted above, the id of the toast increments on each run.
Here is updated test setup:
With this setup the snapshot renders consistently between each run, but the snapshot fails as the id
increments on each run:
it('correctly renders a success notification', () => {
const clock = sinon.useFakeTimers();
class App extends Component {
render(){
return (
<div>
<ToastContainer />
</div>
);
}
}
wrapper = mount(<App />);
toast("Wow so easy !");
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
clock.restore();
wrapper.unmount();
});
# snaphot failure output
AssertionError: expected value to match snapshot correctly renders a success notification 1
+ expected - actual
closeToast={[Function]}
draggable={true}
draggablePercent={80}
hideProgressBar={false}
- id={4}
+ id={1}
in={true}
isDocumentHidden={false}
- key=".$toast-4"
+ key=".$toast-1"
onClose={[Function]}
onExited={[Function]}
onOpen={[Function]}
pauseOnHover={true}
@fkhadra Great thanks yes will do that below. Note I am now calling toast outside of the React app, as you suggested though the <ToastContainer />
is still rendered inside React, so will the unmount reset the id of the toast?
Test setup
npm run tdd
, check the genereated .snap
file for full render including toastm then trigger a test change > observe failure above due to id
increment on new test runpackage.json
:
{
"scripts": {
"test:unit:client": "BABEL_ENV=test mocha --opts configuration/mocha/mocha.client.opts client/**/*.test.js",
"tdd": "npm run test:unit:client -- --watch"
},
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-toastify": "^4.0.0"
},
"devDependencies": {
"babel-polyfill": "^6.26.0",
"babel-register": "^6.26.0",
"chai": "^4.1.2",
"chai-enzyme": "^1.0.0-beta.1",
"chai-jest-snapshot": "^2.0.0",
"dirty-chai": "^2.0.1",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.4",
"jsdom": "^11.11.0",
"mocha": "^5.2.0",
"sinon": "^6.0.0"
}
}
configuration/mocha/mocha.client.opts
:
--colors
--check-leaks
--require babel-register
--require babel-polyfill
--globals SOCKET_URI
--require configuration/mocha/setup.dom.js
configuration/mocha/setup.test.js
configuration/mocha/setup.dom.js:
const { JSDOM } = require('jsdom');
global.window = new JSDOM('').window;
global.document = global.window.document;
Object.keys(global.document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
global[property] = global.document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js',
};
global.HTMLElement = global.window.HTMLElement;
global.requestAnimationFrame = (callback) => {
setTimeout(callback, 0);
};
configuration/mocha/setup.test.js
:
import chai from 'chai';
import chaiEnzyme from 'chai-enzyme';
import dirtyChai from 'dirty-chai';
import chaiJestSnapshot from 'chai-jest-snapshot';
import sinon from 'sinon';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
global.chai = chai;
global.expect = chai.expect;
global.sinon = sinon;
Enzyme.configure({ adapter: new Adapter() });
chai.use(chaiEnzyme());
// jest-snapshot integration
chai.use(chaiJestSnapshot);
chai.use(dirtyChai);
/* global before beforeEach */
before(function globalBefore() {
chaiJestSnapshot.resetSnapshotRegistry();
});
beforeEach(function globalBeforeEach() {
const { title, file } = this.currentTest;
chaiJestSnapshot.setFilename(file.replace('test.js', 'snap'));
chaiJestSnapshot.setTestName(title);
});
notification/notification.test.js
:
import React, { Component } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import { mount } from 'enzyme';
import toJson from 'enzyme-to-json';
describe('@Component - notify', () => {
let wrapper;
it('correctly renders a success notification', () => {
const clock = sinon.useFakeTimers();
class App extends Component {
render(){
return (
<div>
<ToastContainer />
</div>
);
}
}
wrapper = mount(<App />);
toast("Wow so easy !");
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
clock.restore();
wrapper.unmount();
});
});
If i have missed anything or you have any issues setting this up give me a shout
Indeed, the unmount should reset the id of the toast, at least this is what I plan to do 😁.
Also, notice that with the following implementation, your are calling toast
before the ToastContainer
is mounted(first route):
render(){
return (
<div>
{this.notify() /* same as doing toast('hello')*/}
<ToastContainer />
</div>
);
}
In that case, when the container is not mounted the notification are stacked and rendered as soon as the container is mounted. This explain the inconsistency for your test regardless the toastId issue.
Hey @leanne2 ,
The v4.2 clean all the reference and reset the id when the ToastContainer
is umounted.
Thanks for the update. Unfortunately there is still the same issue in my test suite. If i run my tests up they all pass first runs. However in both single-run and watch mode subsequent re runs cause test failures due to the id and toast key incrementing from 1 - 2 between test runs for example:
1) @Component - Notification
correctly renders a success notification:
AssertionError: expected value to match snapshot correctly renders a success notification 1
+ expected - actual
closeToast={[Function]}
draggable={false}
draggablePercent={0}
hideProgressBar={true}
- id={2}
+ id={1}
in={true}
- key=".$toast-2"
+ key=".$toast-1"
onClose={[Function]}
onExited={[Function]}
onOpen={[Function]}
pauseOnFocusLoss={true}
at Context.<anonymous> (src/components/Notification/Notification.test.js:57:32)
The tests that fail and the number that fail changes on each run but the failure reason on each is always the same. This points to a race condition. Is your teardown doing something async? My test setup is as below. You can see I am unmounting the <ToastContainer />
after each test run:
describe(@Component - Notification', () => {
let wrapper;
let clock;
const sandbox = sinon.createSandbox();
class App extends Component {
render() {
return (
<div>
<ToastContainer />
</div>
);
}
}
beforeEach(() => {
clock = sandbox.useFakeTimers();
wrapper = mount(<App />);
});
afterEach(() => {
sandbox.restore();
wrapper.unmount();
});
it('renders string children nested in a paragraph', () => {
notifySuccess('Success!');
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
});
it('renders node children without a paragraph', () => {
notifySuccess(<div>Success!</div>);
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
});
it('passes status prop to notification body', () => {
notifySuccess(<strong>Success!</strong>);
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
});
it('correctly renders a success notification', () => {
notifySuccess('Success!');
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
});
it('correctly renders an info notification', () => {
notifyInfo('Some information');
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
});
it('correctly renders a warning notification', () => {
notifyWarning('Something not quite right!');
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
});
it('correctly renders a failure notification', () => {
notifyFailure('There was an error!');
clock.tick(5);
wrapper.update();
expect(toJson(wrapper)).to.matchSnapshot();
});
});
The functions e.g. notifySuccess
simply delegate to calling a pre-configured toast()
call. So i am still unable to run tests against the toasts. I cannot share my repo but the steps are as follows:
@leanne2 I reopened the issue. Thanks for the update
Hello, I've made a common component sort of thing for Rendering the toast. This is what it returns.
<>
{toast.error(displayToast,{
hideProgressBar: hideProgressBar,
position: position,
closeOnClick:closeOnClick,
draggable:draggable
})}
<ToastContainer/>
</>
I'm trying to write test cases for the above snippet where I just run the method that triggers the Toast
I'm testing it using ReactTestUtils and jest .
The test that I have written is :
describe('Notification', () => {
it('should render Default Notification', () => {
const result = Toast({displayContent:{text:'Without ProgressBar'},notifyProps:{ hideProgressBar:true },type:'default'})
const tree = renderer.create(result);
console.log(tree.toJSON());
})
})
and when I try console log the tree.toJSON() this is what it returns
[
'qcwt2sybdd',
{
type: 'div',
props: { className: 'Toastify', id: undefined },
children: null
}
]
How do I test what is being rendered in the toast? Or what is the text that toast renders?
I'm trying to test my implementations of toasts, namely the rendering (using snapshots but this is not especially relevant). The problem is that if i call the toast function in my test then render the output the toasts are not present in the output. I believe this might be because the library is event driven and so async (though could be wrong). What i would like is access to an API that allows me to test the rendered output of the call to
toast
synchronously. Because toast exports only a function (not React components) its not clear how this can be achieved with the library currently. Could you consider either a) documenting how this can be achieved with the existing API without hacks such assetTimeout
being required in tests, or b) update the API / library to provide some test utils for testing notification rendering synchronously?Here is my test implementation. Note the snapshots are written using
chai-jest-snapshot
Sync tests
Async tests