Unit testing React Bootstrap modal dialog - unit-testing

I'm trying to unit test React Bootstrap modal dialog using Jasmine. But it is not working as expected.
Here is jsfiddle link using latest versions of React, React Bootstrap, Jasmine.: http://jsfiddle.net/30qmcLyf/3/
Test which fails:
line# 27-28
// This test fails. Find DOM Node.
var instanceDomNode = ReactDOM.findDOMNode(instance);
expect(instanceDomNode).not.toBe(null);
line# 39-40
//This test fails. Find modal header.
var headerComponents = TestUtils.scryRenderedComponentsWithType(component, ReactBootstrap.Modal.Header);
expect(headerComponents.length).not.toBe(0);
Also what is wrong with line#35-36. If I uncomment lines I get error shown in comments.
// Error: Did not find exactly one match for componentType:function ModalHeader()...
//var headerComponent = TestUtils.findRenderedComponentWithType(component, ReactBootstrap.Modal.Header);
//expect(headerComponent).not.toBe(null);
As per latest official documentation for test utilities (link), you are supposed to pass ReactComponent as first argument.
Can somebody tell me what is wrong?

Check out how the react-bootstrap team writes tests for this. The modal is rendered into a different subtree which is how it gets rendered to the document body and not directly as a child of its parent. In other words your srcying fails because the component is not in that Component tree.
You can use refs on the modal or look for the DOM nodes directly in the document.

React-Bootstrap modal can be unit tested using mount of enzyme
it(componentToTest.title + 'renders Modal component', () => {
expect(wrapper.find(UVModal).length).toEqual(1);
});
it(componentToTest.title + 'renders major html elements', () => {
// Test whether modal-content element has 3 html children elements.
expect(wrapper.find('.modal-content').length).toEqual(1);
expect(wrapper.find('.modal-content').children()).toHaveLength(3);
// Test whether modal-header element has 2 html children elements.
expect(wrapper.find('.modal-header').length).toEqual(1);
expect(wrapper.find('.modal-header').children()).toHaveLength(2);
// Test whether modal-body element has 1 html child element.
expect(wrapper.find('.modal-body').length).toEqual(1);
expect(wrapper.find('.modal-body').children()).toHaveLength(1);
// Test whether modal-footer element has 1 html child element.
expect(wrapper.find('.modal-footer').length).toEqual(1);
expect(wrapper.find('.modal-footer').children()).toHaveLength(1);
elementToSearch = <p>Lannisters always pay their debt</p>;
expect(wrapper.contains(elementToSearch)).toEqual(false);
});
Check following blog for details:
https://medium.com/#yuvi1422/unit-test-react-bootstrap-modal-a37bf59732ab

In case you are using an older version of Enzyme, you can pass the container element to mount where you want your Modal to be rendered, like this:
Actual Code:
------------
import React from 'react'
import { Modal } from 'reactstrap'
export default MyModal = () => {
return (
<Modal isOpen={props.isOpen}>
<ModalHeader>Header</ModalHeader>
<ModalBody>Body</ModalBody>
</Modal>
);
}
Unit Test:
----------
import React from 'react'
import MyModal from './MyModal'
import { mount } from 'enzyme'
describe(() => {
let wrapper;
beforeEach(() => {
const container = document.createElement("div");
document.body.appendChild(container);
wrapper = mount( <MyModal isOpen={true}/> , {attachTo: container});
});
it('renders correctly', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ModalHeader')).toHaveLength(1);
expect(wrapper.find('ModalBody')).toHaveLength(1);
});
})

Related

Why won't Trix editor mount in Vue component when running tests with Jest?

I built a simple Vue component that wraps the Trix editor. I'm trying to write tests for it, but Trix doesn't seem to mount properly and isn't generating a toolbar element like it does in the browser. I'm using Jest test runner.
TrixEdit.vue
<template>
<div ref="trix">
<trix-editor></trix-editor>
</div>
</template>
<script>
import 'trix'
export default {
mounted() {
let el = this.$refs.trix.getElementsByTagName('trix-editor')[0]
// HACK: change the URL field in the link dialog to allow non-urls
let toolbar = this.$refs.trix.getElementsByTagName('trix-toolbar')[0]
toolbar.querySelector('[type=url]').type = 'text'
// insert content
el.editor.insertHTML(this.value)
el.addEventListener('trix-change', e => {
this.$emit('input', e.target.innerHTML)
})
}
}
</script>
TrixEdit.spec.js
import { mount, shallowMount, createLocalVue } from '#vue/test-utils'
import TrixEdit from '#/components/TrixEdit.vue'
const localVue = createLocalVue()
localVue.config.ignoredElements = ['trix-editor']
describe('TrixEdit', () => {
describe('value prop', () => {
it('renders text when value is set', () => {
const wrapper = mount(TrixEdit, {
localVue,
propsData: {
value: 'This is a test'
}
})
expect(wrapper.emitted().input).toEqual('This is a test')
})
})
})
The expect() fails with the following error
Expected value to equal:
"This is a test"
Received:
undefined
at Object.toEqual (tests/unit/TrixEdit.spec.js:19:39)
Why is Trix not initializing in my test?
trix-editor is not mounting mainly because MutationObserver is not supported in JSDOM 11, and attachToDocument was not used. There were several other bugs in the test described below.
GitHub demo w/issues fixed
Missing MutationObserver and window.getSelection
Vue CLI 3.7.0 uses JSDOM 11, which doesn't support MutationObserver, needed by the Custom Elements polyfill to trigger the connectedCallback. That lifecycle hook normally invokes trix-editor's initialization, which would create the trix-toolbar element that your test is trying to query.
Solution 1: In your test, import mutationobserver-shim before TrixEdit.vue, and stub window.getSelection (called by trix and currently not supported by JSDOM):
import 'mutationobserver-shim' // <-- order important
import TrixEdit from '#/components/TrixEdit.vue'
window.getSelection = () => ({})
Solution 2: Do the above in a Jest setup script, configured by setupTestFrameworkScriptFile:
Add the following property to the config object in jest.config.js (or jest in package.json):
setupTestFrameworkScriptFile: '<rootDir>/jest-setup.js',
Add the following code to <rootDir>/jest-setup.js:
import 'mutationobserver-shim'
window.getSelection = () => ({})
Missing attachToDocument
#vue/test-utils does not attach elements to the document by default, so trix-editor does not catch the connectedCallback, which is needed for its initialization.
Solution: Use the attachToDocument option when mounting TrixEdit:
const wrapper = mount(TrixEdit, {
//...
attachToDocument: true, // <-- needed for trix-editor
})
Premature reference to trix-editor's editor
TrixEdit incorrectly assumes that trix-editor is immediately initialized upon mounting, but initialization isn't guaranteed until it fires the trix-initialize event, so accessing trix-editor's internal editor could result in an undefined reference.
Solution: Add an event handler for the trix-initialize event that invokes the initialization code previously in mounted():
<template>
<trix-editor #trix-initialize="onInit" />
</template>
<script>
export default {
methods: {
onInit(e) {
/* initialization code */
}
}
}
</script>
Value set before change listener
The initialization code adds a trix-change-event listener after the value has already been set, missing the event trigger. I assume the intention was to also detect the first initial value setting in order to re-emit it as an input event.
Solution 1: Add event listener first:
<script>
export default {
methods: {
onInit(e) {
//...
el.addEventListener('trix-change', /*...*/)
/* set editor value */
}
}
}
</script>
Solution 2: Use v-on:trix-change="..." (or #trix-change) in the template, which would remove the setup-order problem above:
<template>
<trix-editor #trix-change="onChange" />
</template>
<script>
export default {
methods: {
onChange(e) {
this.$emit('input', e.target.innerHTML)
},
onInit(e) {
//...
/* set editor value */
}
}
}
</script>
Value setting causes error
The initialization code sets the editor's value with the following code, which causes an error in test:
el.editor.insertHTML(this.value) // causes `document.createRange is not a function`
Solution: Use trix-editor's value-accessor, which performs the equivalent action while avoiding this error:
el.value = this.value
I can confirm the problem lies in the not-so-ideal polymer polyfill included inside trix lib. I did an experiment to force apply the polyfill, then I can reproduce the same error TypeError: Cannot read property 'querySelector' of undefined even inside chrome browser env.
Further investigation narrows it down to the MutationObserver behavior difference, but still haven't got to the bottom.
Way to reproduce:
TrixEdit.vue
<template>
<div ref="trix">
<trix-editor></trix-editor>
</div>
</template>
<script>
// force apply polymer polyfill
delete window.customElements;
document.registerElement = undefined;
import "trix";
//...
</script>

How to Test a Global Event Bus With Vue Test Utils?

I am trying to learn how to test events emitted through a global Event Bus. Here's the code with some comments in the places I don't know what to do.
// EvtBus.js
import Vue from 'vue';
export const EvtBus = new Vue();
<!-- CouponCode.vue -->
<template>
<div>
<input
class="coupon-code"
type="text"
v-model="code"
#input="validate">
<p v-if="valid">
Coupon Redeemed: {{ message }}
</p>
</div>
</template>
<script>
import { EvtBus } from '../EvtBus.js';
export default {
data () {
return {
code: '',
valid: false,
coupons: [
{
code: '50OFF',
discount: 50,
message: '50% Off!'
},
{
code: 'FREE',
discount: 100,
message: 'Entirely Free!'
}
]
};
},
created () {
EvtBus.$on('coupon-applied', () => {
//console.info('had a coupon applied event on component');
});
},
methods: {
validate () {
// Extract the coupon codes into an array and check if that array
// includes the typed in coupon code.
this.valid = this.coupons.map(coupon => coupon.code).includes(this.code);
if (this.valid) {
this.$emit('applied');
// I NEVER see this on the coupon-code.spec.js
EvtBus.$emit('coupon-applied');
}
}
},
computed: {
message () {
return this.coupons.find(coupon => coupon.code === this.code).message;
}
}
}
</script>
// tests/coupon-code.spec.js
import expect from 'expect';
import { mount } from '#vue/test-utils';
import CouponCode from '../src/components/CouponCode.vue';
import { EvtBus } from '../src/EvtBus.js';
describe('Reminders', () => {
let wrp;
beforeEach(() => {
wrp = mount(CouponCode);
});
it('broadcasts the percentage discount when a valid coupon code is applied', () => {
let code = wrp.find('input.coupon-code');
code.element.value = '50OFF';
code.trigger('input');
console.log(wrp.emitted('applied'));
//
// I NEVER see this on the outpout.
// How can I test it through a global event bus rather than
// an event emitted from the component instance?
//
EvtBus.$on('coupon-applied', () => {
console.log('coupon was applied through event bus');
});
// Passes, but not using EvtBus instance.
expect(wrp.emitted('applied')).toBeTruthy;
});
});
So, my doubt is how to test that the global event bus is emitting and listening to events inside components that use that event bus.
So, is it possible to test the global Event Bus using Vue Test Utils or I should use another approach?
If component is using global EventBus, eg that's imported outside of given component and assigned to window.EventBus, then it's possible to use global Vue instance to redirect $on or $emit events to wrapper's vm instance. That way you can proceed writing tests as if component is emitting via this.$emit instead of EventBus.$emit:
it('clicking "Settings" button emits "openSettings"', () => {
global.EventBus = new Vue();
global.EventBus.$on('openSettings', (data) => {
wrapper.vm.$emit('openSettings', data);
});
// component emits `EventBus.$emit('openSettings')`
expect(wrapper.emitted('openSettings')).toBeTruthy(); // pass
});
Well,
EvtBus.$on('coupon-applied', () => {
console.log('coupon was applied through event bus');
});
This code in your spec file won't be called because the mounted wrp component is not using the same EvtBus you are importing in your spec file above.
What you require to test this is an npm package named inject-loader so that you can provide your own implementation(stub) of the EvtBus dependency of your coupon code component.
Somewhat like this
const couponCodeInjector = require('!!vue-loader?inject!src/views/CouponCode');
const stubbedModules = {
'../EvtBus.js': {
$on : sandbox.spy((evtName, cb) => cb());
}
};
const couponCode = couponCodeInjector(stubbedModules);
and then in your unit test you can assert whether the stubbedModules['../EvtBus.js'].$on has been called or not when code.trigger('input');
PS: I haven't used vue-test-utils. So I don't know exactly how to the stubbing with this npm package.
But the main thing you need to do is to find a way to stub your EvtBus dependency in the CouponCode component in such a way that you can apply a spy on it and check whether that spy has been called or not.
Unit tests should focus on testing a single component in isolation. In this case, you want to test if the event is emitted, since that is the job of CouponCode.vue. Remember, unit tests should focus on testing the smallest units of code, and only test one thing at a time. In this case, we care that the event is emitted -- EventBus.test.js is where we test what happens when the event is emitted.
Noe that toBeTruthy is a function - you need (). expect(wrp.emitted('applied')).toBeTruthy is actually not passing, since you need () - at the moment, it is actually doing nothing -- no assertion is made.
What your assertion should look like is:
expect(wrp.emitted('applied')).toBeTruthy()
You can go one step further, and ensure it was only emitted once by doing something like expect(wrp.emitted().applied.length).toBe(1).
You then test InputBus in isolation, too. If you can post the code for that component, we can work through how to test it.
I worked on a big Vue app recently and contributed a lot to the main repo and documentation, so I'm happy to help out wherever I can.
Let me know if that helps or you need more guidance. If possible, post EventBus.vue as well.
I got the same issue with vue-test-utils and Jest. For me, createLocalVue() of vue-test-utils library fixed the issue. This function creates a local copy of Vue to use when mounting the component. Installing plugins on this copy of Vue prevents polluting the original Vue copy. (https://vue-test-utils.vuejs.org/api/options.html#localvue)
Adding this to your test file will fix the issue:
const EventBus = new Vue();
const GlobalPlugins = {
install(v) {
// Event bus
v.prototype.$bus = EventBus;
},
};
// create a local instance of the global bus
const localVue = createLocalVue();
localVue.use(GlobalPlugins);
jest.mock('#/main', () => ({
$emit: jest.fn(),
}));
Include this in code in your spec file at the very begining.
Note: '#/main' is the file from which you are importing Event Bus.

Enzyme / React Unit test

I have this React functional UI only component, which has two props passed in, the second being a function that is passed from its parent component. The onClick calls 'delegates' to a function in the parent container component, this parent method is then responsible for dispatching to a redux store.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
const BotShowUI = ({ bot, onClick }) => {
return(
<div id={bot.id} onClick={onClick}>
{bot.id} : {bot.text}
</div>
)
}
BotShowUI.propTypes = {
bot: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired
};
export default BotShowUI;
My test spec is, which uses Jasmine
import React, {Component} from 'react';
import { mount } from 'enzyme';
import BotShowUI from '../botShowUI';
function onClickFunction(){};
describe('botShowUI', () => {
const bot = {id: 1, isDone: false, text: 'bot 123'};
const expectedDivText = '1 : bot 123';
const wrapper = mount(<BotShowUI bot={bot} onClick={onClickFunction} />);
it(' div has been rendered ', () => {
expect(wrapper.find('div').first()).not.toBe(null);
});
it(' div displays the correct bot text ', () => {
expect(wrapper.find('div').first().text()).toEqual(expectedDivText)
});
it(' div click event fired ', () => {
wrapper.simulate('click');
expect(wrapper.state('onClick')).toBe(true);
});
});
This last assertion fails with
Chrome 57.0.2987 (Windows 10 0.0.0) botShowUI div click event fired FAILED
TypeError: Cannot read property 'onClick' of null
at ReactWrapper.state (webpack:///~/enzyme/build/ReactWrapper.js:825:24 <- tests.webpack.js:26303:25)
at Object.<anonymous> (webpack:///app/react/components/bots/_tests/botShowUI.spec.js:25:23 <- tests.webpack.js:25415:25)
wrapper.simulate('click'); works, but the next line fails
What is the correct way to assert that the click was fired ?
Do I have to drop into wrapper's props/children instead of using state ?
I'm not trying to test the parent container in any way, the two are isolated.
This test is only concerned with this UI component.
First thing is that onClick isn't on state, but on props, so you will have to access it by doing wrapper.props('onClick').
Secondly, to test whether onClick has been handled or not is to use a spy, rather than an empty function. If you do not want to use spy, you can still do that, but not the way you have done. If you are interested, I can post some pseudo-code for that too. But coming back to using spies, you can use a spy as the onClick prop. Below is the code for that. I have hand-written it, so please check for any syntax error, but you should get the idea on what needs to be done.
it('should call the onClick handler on click', () => {
const onClickFunction = sinon.spy()
wrapper = mount(<BotShowUI bot={bot} onClick={onClickFunction} />)
wrapper.simulate('click');
expect(onClickFunction).toHaveBeenCalled();
})
Based on Abhishek's answer here's my solution for Jasmine
it(' div click event fired ', () => {
let onClickFunction_spy = jasmine.createSpy('onClickFunction');
const wrapper = mount(<BotShowUI bot={bot} onClick={onClickFunction_spy} />);
wrapper.simulate('click');
expect(onClickFunction_spy).toHaveBeenCalled();
});
Hope this helps anyone.

Testing a sub-component with a Link: '<Link>s rendered outside of a router context cannot navigate.'

I have a button component that creates a react-router Link element. It also allows an onClick function to be passed in for additional functionality (e.g. sending a Google Analytics event).
I have included this component in a parent, like so:
export default class Page extends Component {
const doSomething = () => {
//do a thing to test here
}
return (
<div>
<Button
onClickFn{() => doSomething()}
linkToUrl='/other/page' //this creates a <Link> inside the button
/>
</div>
)
}
Problem comes when I want to test that doSomething is being triggered correctly. I have used Enzyme mount to create the test Page component including the button. When I simulate a click I get the following error
'<Link>s rendered outside of a router context cannot navigate.'
because the Link in the button has no context. Is there a way of mocking this or preventing the error from showing? Or is there a better way of testing this functionality?
In your test, you will need to render the component within a <Router>. You can take a look at the tests for the <Link> component for examples on how to do this.
The basic idea is to create a memory history instance, pass that to the <Router>, and then render the <Link> inside of a <Route> passed to that. It sounds a bit involved, but it is fairly simple.
import { createMemoryHistory } from 'history'
it('clicks', () => {
const history = createMemoryHistory()
const App = () => (
<Router history={history}>
<Route path='/' component={Page} />
</Router>
)
})
Building on top of Paul's answer, here's a more detailed example for testing the onClick of a Button (or its Link child to be more precise). The example uses the testing libraries mocha (BDD test runner), chai (BDD assertions), enzyme (React testing utility), and sinon (test doubles).
import React from 'react';
import { Router, Route } from 'react-router';
import { createMemoryHistory } from 'history';
import MyCustomPage from '/.index';
describe('MyCustomPage', function(){
it('stores data when clicking the link', function() {
// Arrange
const Page = () => (
<MyCustomPage foo="foo" bar="bar" />
);
const container = enzyme.mount(
<Router history={createMemoryHistory()}>
<Route path="/" component={Page} />
</Router>
);
// Act
container.find('#my-link').simulate('click');
// Assert
expect(sessionStorage.setItem).to.have.been.calledWith('key', 'value');
});
});

How to test for a `click` event on the icon button of the `material-ui` AppBar?

I'm testing the following React component:
import React from 'react'
import AppBar from 'material-ui/lib/app-bar'
class NavBar extends React.Component {
render () {
return (
<div>
<AppBar
title='My NavBar Title'
/>
</div>
)
}
}
export default NavBar
It's composed of a material-ui AppBar component. Using Tape and Enzyme, I want to simulate a click on the AppBar's IconButton:
import NavBar from './NavBar'
import React from 'react'
import test from 'tape'
import { /* I don't know if it's `shallow` or `mount` */ } from 'enzyme'
test('NavBar component test', (assert) => {
test('simulating a click on the icon button', (assert) =>
// How do I do this?
// The following results in error:
// const wrapper = shallow(<NavBar />)
// wrapper.find('AppBar').find('IconButton').simulate('click')
assert.end()
})
assert.end()
})
How can I do it properly?
Obs: I'm searching for IconButton because, according to the React Dev Tools tab, that's the name of the rendered icon button component.
You should use mount for testing components below the top level of the component.
I found a way to test whether the function is called, not using the .simulate('event'), just invoke the method directly.
const wrapper = shallow(<NavBar />)
//use sinon.spy( object, method) to spy the method, instead of sinon.spy(func)
const spy = Sinon.spy(wrapper.renderer._instance._instance, 'click')
//inovke
wrapper.renderer._instance._instance.click()
expect(spy.called).to.be.true
You could find the method inside the .renderer._instance or its children _instance objects (depending on how deep the element is) and then use sinon.spy to spy this method.
I don't like this way, but this is the only way I know how to spy a method till now.