I'm new to Jest and trying to write a test for my Vue app that would confirm a child component emits an event and as a result, its prop value gets updated.
As an example I've made a simple app with a counter to demonstrate:
/* Parent Home.vue */
<template>
<Counter :count="count" #increment="count++"/>
</template>
<script>
import Counter from "#/components/Counter.vue";
export default {
components: { Counter },
data: () => ({
count: 0,
}),
};
</script>
/* Child Counter.vue */
<template>
<v-container>
<div id="propTracker">{{ count }}</div>
<v-btn ref="incrementProp" #click="increment($event)">Increase prop</v-btn>
</v-container>
</template>
<script>
export default {
props: ["count"],
methods: {
increment() {
this.$emit("increment");
},
},
};
</script>
Once the button in Counter is pressed, it should emit an increment event to increment the count in parent Home component.
This is the test I've written:
it("Click should increment count text", async () => {
const wrapper = mount(Counter, {
localVue,
vuetify,
propsData: { count: 0 },
});
expect(wrapper.find("#propTracker").text()).toBe("0"); //initial state
const button = wrapper.find({ ref: "incrementProp" });
await button.trigger("click"); //trigger click
expect(wrapper.find("#propTracker").text()).toBe("1"); //after click
});
It returns back as Expected: "1" Received: "0" indicating that the prop update is not processed within the test. I've tried to combine many resources such as Vue guidelines here and Vuetify unit test info here, yet it keeps coming back the same. I'm missing a puzzle piece and been looking for it for 2 days now.
This is a simplified repo to have a better picture and perhaps play around locally.
A test to see if incrementing works with local data values worked fine: here, so it's really the scenario with props and emitting that bums me out at the moment. Any help would be worth its weight in gold!
I downloaded your repository and did the tests and everything worked fine. with the exception of the title that did not have a defined variable.
See print: https://puu.sh/GKmzu/69a9fe9f0a.png
I think you should test just if the event is emitted. See that if you test for the local counter, the test will pass, but not for the prop count. That's because the test is not seeing the code on the Home component. Remember this is a unit test, the goal is to test the component in isolation.
Okay, I finally found a solution! I went the wrong way about things and tried to test parent data changes in a child component. With the composition I have (child component Counter emits an event and triggers a change in parent Home component), this is the working test:
it("Counter button changes count in Home", () => {
const wrapper = mountFactory(Home);
//check if initial count in Home is 0
expect(wrapper.vm.count).toBe(0);
//click btn in Counter (child component)
wrapper.find("#incrementBtn").trigger("click");
//check if increment worked and count has increased
expect(wrapper.vm.count).toBe(1);
});
Learning curve to think in "Jest" :)
Related
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.
This doesn't work for me. Please make the Plunkr below work.
describe("trying a test", () => {
beforeEach(() => {
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
TestBed.configureTestingModule({
declarations: [myCmp, ChildCmp]
});
});
it("test should work", () => {
const fixture = TestBed.createComponent(myCmp);
const div = fixture.debugElement.children[0];
const childCmp = div.queryAll(By.directive(ChildCmp));
const divEl = div.queryAll(By.css('div'));
divEl[0].triggerEventHandler('click', <Event>{});
fixture.detectChanges();
expect(childCmp[0].nativeElement.textContent).toBe("updated value");
});
});
https://plnkr.co/edit/wWJMDi3ZFC6RTSvCw4HH?p=preview
This is no longer an issue for me. For the most part (with one exception I will outline below), both my parent components and child components get updated in my real app code, though I haven't updated the Plunkr.
I just use .and.callThrough() on the spyed on parent component method that is called, when something is clicked on the template. Rather than a simple spy.
I do have an issue with ngFor rendered child components not updating. This is my other question: Angular 2 unit testing: Make ngFor rendered, child components' templates change?
React Tests Fails after set State causes second render
Up until now testing has been going well with JSDOM and Mocha. So far have not had to test any components that change their state. I found my first issue testing a component that changes it's state.
The Error
1) Reduced Test Case - #current Tests that Fail when Component changes state and renders "before each" hook:
Error: Invariant Violation: dangerouslyRenderMarkup(...): Cannot render markup in a worker thread. Make sure `window` and `document` are available globally before requiring React when unit testing or use React.renderToString for server rendering.
at Context.<anonymous> (test/react-reflux/parts/Reduced-spec.js:47:32)
The Component : Reduced.js
var React = require('react');
var Reduced = React.createClass({
getInitialState() {
console.log("start off with editing as false");
return {editing: false};
},
edit() {
console.log("Setting State to Edit");
this.setState({editing: true});
},
render() {
console.log("Rendering");
return (
<span onClick={this.edit}>
{(this.state.editing) ? "Editing" : "Click To Edit"}
</span>
);
}
});
module.exports = Reduced;
The Tests : 1-pass, 1-fail
var React, TestUtils, jsdom, Reduced, expect;
describe('Reduced Test Case', function () {
before(function () {
jsdom = require('jsdom');
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = global.document.parentWindow;
React = require('react/addons');
TestUtils = React.addons.TestUtils;
Reduced = require('./Reduced');
expect = require('chai').expect;
this.component = TestUtils.renderIntoDocument(
<Reduced />
);
var root = TestUtils.findRenderedDOMComponentWithTag(this.component, 'span');
this.el = root.getDOMNode();
});
describe("Tests Pass without simulate", function () {
it("Root Element Reads 'Click To Edit'", function () {
expect(this.el.innerHTML).to.equal('Click To Edit');
});
});
describe("Tests that Fail when Component changes state and renders", function () {
beforeEach(function () {
//
// Simulate invokes edit, invokes set state, invokes render, then error occures
//
TestUtils.Simulate.click(this.el);
});
it("Root Element Reads 'Editing'", function () {
expect(this.el.innerHTML).to.equal('Editing');
});
});
});
The Results
> mocha --compilers js:babel/register
Reduced Test Case - #current
start off with editing as false
Rendering
Tests Pass without simulate
✓ Root Element Reads 'Click To Edit'
Tests that Fail when Component changes state and renders
Setting State to Edit
Rendering
1) "before each" hook
1 passing (379ms)
1 failing
1) Reduced Test Case Tests that Fail when Component changes state and renders "before each" hook:
Error: Invariant Violation: dangerouslyRenderMarkup(...): Cannot render markup in a worker thread. Make sure `window` and `document` are available globally before requiring React when unit testing or use React.renderToString for server rendering.
at Context.<anonymous> (test/Reduced-spec.js:47:32)
I've been going crazy
Everything is loaded after global.window and global.document
The Simulate Event invokes edit(), then render() before error
All React Mocha JSDOM tests have been working well until this state change issue
Please help ???
The setup JSDOM setup was missing global.navigator.
global.navigator = {
userAgent: 'node.js'
};
Insert your global object modifying(passing window and document objects to global) before React is required.
Because React creates its ExecutionEnvironment object while required and don't modify it while works.
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);
});
})
In my JS view-code I am using a jQuery UI Dialog component to render a popup.
I instantiate it like this:
var popupDialog = $("#myPopupDiv").dialog({
title: "My dialog",
dialogClass: "myDialogClass",
create: createHandler,
draggable: false,
width: width,
height: height,
autoOpen: false
});
Notice it's got autoOpen set to "false". I open it in the "create"-handler:
var createHandler = function(event, ui) {
//Vi venter litt for å sikre at popupen er "klar"
setTimeout(function () {
popupDialog.dialog("open");
}, 5);
};
The open-logic is wrapped in a setTimeout to ensure the popup is ready.
The code works fine in app the browser, but when I run this code using Jasmine test-framework I get an error:
Error: cannot call methods on dialog prior to initialization; attempted to call method 'open'
The test actually passes, so clearly the item is rendered. But I don't like the error showing up when I run the tests!
I suspect that since the Jasmine tests run so fast, the component has not had time to initialize itself. So how can I assure that the component is initialized? I thought putting this logic in the "create"-handler would take care of that since that event is "Triggered when the dialog is created.", but clearly that is not the case.
Here is how I test it:
it("should show my popup", function () {
var myPopupLink = $('.popupLink');
myPopupLink.click();
//Wait until popup is shown
waitsFor(function () {
return !$('.myDialogClass').is(":hidden");
}, "Popupen didn't show", 1000);
//Check that the DOM is as expected
expect($('.myDialogClass .popupContentDiv')).toExist();
expect(...
//Close popup
myPopupLink.click();
expect($('.myDialogClass .popupContentDiv')).not.toExist();
});
Anybody have a clue how I can verify the initialization-status of the popup-dialog?
Or any other workarounds?
Thanks!
The problem with your test is, that it is more an acceptance test then a unit test. Most of stuff that you try to test is functionality of jQueryUi. What you really wanna test is that the createHandler opened the dialog with a delay. So your popupDialog.dialog should be a spy where you can check that it was called after the delay.
At the moment your code is really hard to test cause it is based directly on jquery. You should think about to have functions where you can inject your depenedencies instead of relying on global variables like popupDialog.
Here is an example on how to mock out all dependencies:
//mock out setTimeout so you dont have to wait in your test
jasmine.Clock.useMock();
//create a mock that will return from $().dialog()
var mockDialog = jasmine.createSpy('dialog');
// mock $ to return {dialog: mock that return {dialog: mockDialog}}
var mock$ = spyOn(window, '$').andReturn({
dialog:jasmine.createSpy('$').andReturn({
dialog: mockDialog
})
})
expect(mock$).toHaveBeenCalled();
// call the create function
window[mock$.mostRecentCall.args[0].create]();
jasmine.Clock.tick(4999);
expect(mockDialog$).not.toHaveBeenCalled();
jasmine.Clock.tick(5001);
expect(mockDialog$).toHaveBeenCalledWith('open');
As you can see its very complicated to mock out all the jQuery dependencies. So ether you rewrite your code for better testability or test this stuff as acceptance test with selenium capybara etc.