I'm testing a simple Modal component which is a wrapper of reactstrap component. I'm using #testing-library/react.
It works ok in browser but during the testing I sawed that the modal permanent exists in DOM even after unmount.
expect(props.toggleModal).toHaveBeenCalled();
Above assertion works good. So we have assurance that this function has been called and the modal state has been changed from true to false.
I checked in my Modal component and this is correct. So modal shouldn't be displayed (it works in browser).
/* eslint-disable react/prop-types */
import React, { useState } from "react";
import { render, fireEvent, cleanup } from "#testing-library/react";
import Modal from "./";
afterEach(cleanup);
const props = {
toggleModal: jest.fn(),
title: "Fake title",
body: (
<div>
<p>Fake body</p>
</div>
),
footer: (
<ul>
<li>Link 1</li>
<li>Link 2</li>
</ul>
)
};
function App() {
const [modal, setModal] = useState(true);
props.toggleModal.mockImplementation(() => setModal(prevModal => !prevModal));
return (
<div id="app">
<Modal {...props} isOpen={modal} />
</div>
);
}
test("renders Modal component", () => {
const { getByText, getAllByText } = render(<App />);
expect(getByText("Fake body")).toBeTruthy();
expect(getAllByText("Link", { exact: false })).toHaveLength(2);
fireEvent.click(document.querySelector("button"));
expect(props.toggleModal).toHaveBeenCalled();
console.log(document.body.innerHTML);
});
//Modal.js
import React from "react";
import PropTypes from "prop-types";
import {
Modal as BootstrapModal,
ModalHeader,
ModalBody,
ModalFooter
} from "reactstrap";
class Modal extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<BootstrapModal
isOpen={this.props.isOpen}
toggle={this.props.toggleModal}
>
{this.props.title && (
<ModalHeader toggle={this.props.toggleModal}>
{this.props.title}
</ModalHeader>
)}
{this.props.body && <ModalBody>{this.props.body}</ModalBody>}
{this.props.footer && <ModalFooter>{this.props.footer}</ModalFooter>}
</BootstrapModal>
);
}
static propTypes = {
isOpen: PropTypes.bool,
title: PropTypes.string,
body: PropTypes.element,
footer: PropTypes.element,
toggleModal: PropTypes.func
};
}
export default Modal;
I think that modal shouldn't exist in DOM so in this example document.body.innerHTML should have only <div><div id="app"></div></div>
Related
I have a component that switch Language of a nuxtjs application using nuxt-i18n as follows
<template>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link langpicker">{{ $t("language_picker") }} </a>
<div class="navbar-dropdown is-hidden-mobile">
<div>
<nuxt-link
v-if="currentLanguage != 'en'"
class="navbar-item"
:to="switchLocalePath('en')"
>
<img src="~/static/flags/us.svg" class="flagIcon" /> English
</nuxt-link>
<nuxt-link
v-if="currentLanguage != 'el'"
class="navbar-item"
:to="switchLocalePath('el')"
>
<img src="~/static/flags/el.svg" class="flagIcon" /> Ελληνικά
</nuxt-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LangPicker",
computed: {
currentLanguage() {
return this.$i18n.locale || "en";
}
}
};
</script>
I want to write a Unit Test that test the correct language switch on 'nuxt-link' click.
So far I have the following
import { mount, RouterLinkStub } from "#vue/test-utils";
import LangPicker from "#/components/layout/LangPicker";
describe("LangPicker with locale en", () => {
let cmp;
beforeEach(() => {
cmp = mount(LangPicker, {
mocks: {
$t: msg => msg,
$i18n: { locale: "en" },
switchLocalePath: msg => msg
},
stubs: {
NuxtLink: RouterLinkStub
}
});
});
it("Trigger language", () => {
const el = cmp.findAll(".navbar-item")
});
});
cmp.find(".navbar-item") return an empty object.
I don't know how I must set up to "trigger" the click event.
const el = cmp.findAll(".navbar-item")[1].trigger("click");
make sure your find selector is correct.
const comp = cmp.find(".navbar-item");
comp.trigger('click');
you can use chrome dev tools selector utility.
Refer this link for detailed information.
I am testing my App.vue , and I am stuck with a Vuex error on getters...
I guess it's related to a badly define getters property , but I don't see how to solve it ..
feeedback welcome
Console.log
ERROR LOG: '[vuex] unknown getter: getLists'
App.vue
✗ calls store action addShoppingList when a click event is fired from the plus-sign icon
AssertionError: expected false to equal true
at Context.<anonymous> (webpack:///test/unit/specs/App.spec.js:33:50 <- index.js:24490:51)
App.spec.js
import App from '#/App'
import Vue from 'vue'
import Vuex from 'vuex'
import sinon from 'sinon'
import { mount } from 'avoriaz'
Vue.use(Vuex)
describe('App.vue', () => {
let actions
let getters
let store
beforeEach(() => {
actions = {
addShoppingList: sinon.stub(),
populateShoppingLists: sinon.stub()
}
getters = {
shoppinglists: () => 'getLists'
}
store = new Vuex.Store({
state: {},
actions,
getters
})
})
it('calls store action addShoppingList when a click event is fired from the plus-sign icon', (done) => {
const wrapper = mount(App, { store })
wrapper.find('a')[0].trigger('click')
wrapper.vm.$nextTick(() => {
expect(actions.createShoppingList.calledOnce).to.equal(true)
done()
})
})
App.vue
<template>
<div id="app" class="container">
<ul class="nav nav-tabs" role="tablist">
<li :class="index===shoppinglists.length-1 ? 'active' : ''" v-for="(list, index) in shoppinglists" :key="list.id" role="presentation">
<shopping-list-title-component :id="list.id" :title="list.title"></shopping-list-title-component>
</li>
<li>
<a href="#" #click="addShoppingList">
<i class="glyphicon glyphicon-plus-sign"></i>
</a>
</li>
</ul>
<div class="tab-content">
<div :class="index===shoppinglists.length-1 ? 'active' : ''" v-for="(list, index) in shoppinglists" :key="list.id" class="tab-pane" role="tabpanel" :id="list.id">
<shopping-list-component :id="list.id" :title="list.title" :items="list.items"></shopping-list-component>
</div>
</div>
</div>
</template>
<script>
import ShoppingListComponent from './components/ShoppingListComponent'
import ShoppingListTitleComponent from './components/ShoppingListTitleComponent'
import store from './vuex/store'
import { mapGetters, mapActions } from 'vuex'
import _ from 'underscore'
export default {
components: {
ShoppingListComponent,
ShoppingListTitleComponent
},
computed: {
...mapGetters({ shoppinglists: 'getLists' })
},
methods: _.extend({}, mapActions(['populateShoppingLists', 'createShoppingList']), {
addShoppingList () {
let list = { title: 'New Shopping List', items: [] }
this.createShoppingList(list)
}
}),
store,
mounted: function () {
this.$nextTick(function () {
this.populateShoppingLists()
})
}
}
</script>
UPDATE
here are my getters.js anf store.js files
store.js is imported in App.vue
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters' // import getters !
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex)
const state = {
shoppinglists: []
}
export default new Vuex.Store({
state,
mutations,
getters,
actions
})
getters.js
import _ from 'underscore'
export default {
getLists: state => state.shoppinglists,
getListById: (state, id) => {
return _.findWhere(state.shoppinglists, { id: id })
}
}
You don't define a getLists getter in your store. You're defining a shoppinglists getter that returns 'getLists'.
You need to either change your mapGetters line to be:
...mapGetters(['shoppinglists'])
Or change the name of the getter to getLists:
getters = {
getLists: () => 'getLists'
}
(Although I'm not sure if you are really meaning to return a string value in that getter or not)
#thanksd put me on tracks... see my comment
so I need to define the getters in my Vur.spec.js as following
getters = {
getLists: () => {
// console.log('WE ARE S TEST')
state => state.shoppinglists
}
}
I am currently testing one of my children components
I already tested successfully all vuex actions, mutations and getters
When I run this ChangeTitleComonent.spec.js , it passes with 100% coverage.. thanks for feedback
Do I need to write some additional tests when it's 100% covered ? or is this test bad written ?
PARENT
ShoppingListComponent.vue
<template>
<div>
<h2>{{ title }}</h2>
<add-item-component :id='id' #add="addItem"></add-item-component>
<items-component :items="items" :id="id"></items-component>
<div class="footer">
<hr />
<change-title-component :title="title" :id="id"></change-title-component>
</div>
</div>
</template>
<script>
import AddItemComponent from './AddItemComponent'
import ItemsComponent from './ItemsComponent'
import ChangeTitleComponent from './ChangeTitleComponent'
export default {
components: {
AddItemComponent,
ItemsComponent,
ChangeTitleComponent
},
props: ['id', 'title', 'items'],
methods: {
addItem (text) {
this.items.push({
text: text,
checked: false
})
}
}
}
</script>
<style scoped>
.footer {
font-size: 0.7em;
margin-top: 20vh;
}
</style>
CHILDREN
ChangeTitleComponent
<template>
<div>
<em>Change the title of your shopping list here</em>
<input :value="title" #input="onInput({ title: $event.target.value, id: id })"/>
</div>
</template>
<style scoped>
</style>
<script>
import { mapActions } from 'vuex'
export default {
props: ['title', 'id'],
methods: mapActions({ // dispatching actions in components
onInput: 'changeTitle'
})
}
</script>
UNIT TEST
ChangeTitleComponent.spec.js
import Vue from 'vue'
import ChangeTitleComponent from '#/components/ChangeTitleComponent'
import store from '#/vuex/store'
describe('ChangeTitleComponent.vue', () => {
describe('changeTitle', () => {
var component
beforeEach(() => {
var vm = new Vue({
template: '<change-title-component :title="title" :id="id" ref="changetitlecomponent">' +
'</change-title-component></div>',
components: {
ChangeTitleComponent
},
props: ['title', 'id'],
store
}).$mount()
component = vm.$refs.changetitlecomponent
})
it('should change the title', () => {
// check component label text
expect(component.$el.textContent).to.equal('Change the title of your shopping list here ')
// simulate input Enter event
const input = component.$el.querySelector('input')
input.value = 'My New Title'
const enterEvent = new window.Event('keypress', { which: 13 })
input.dispatchEvent(enterEvent)
component._watcher.run()
})
})
})
I have a parent component that keeps all the state and that renders my child component inside a Router. I'm passing a function from parent to child as props to handle the button click.
import React, {Component} from 'react';
import 'whatwg-fetch';
import arrayOfObjects from '../arrayOfObjects';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import ChildComponent from './ChildComponent';
class Parent extends Component {
constructor() {
super();
this.fetchObject = this.fetchObject.bind(this);
const tempmyObject = {};
this.state = {
myObject: tempmyObject
};
}
fetchObject(event) {
const that = this;
fetch(path)
....
that.setState(
{
myObject: newObject
});
....
event.preventDefault();
};
render() {
return (
<Router>
<div>
<Route exact path='/GetUserInfo'
render={(props) => (
<ChildComponent {...props}
fields={arrayOfObjects}
myObject={this.state.myObject}
onChange={(event) => this.fetchObject(event)}
buttonName='foo'
/>
)}
/>
</div>
</Router>
);
}
}
export default Parent ;
import ShowChildComponent from './ShowChildComponent';
class ChildComponent extends Component {
render() {
return (
<div>
<ShowChildComponent
fields={this.props.arrayOfObjects}
myObject={this.state.myObject}
buttonName={this.props.name}
onChange={this.props.onChange}
/>
</div>
);
}
}
export default ChildComponent;
I'm using mount from enzyme and I would like to test it and simulate a button click in my unit tests, and then test for the changed data in the paragraph.
Previous to that, I had all state kept in the child component like this :
import React, {Component} from 'react';
import 'whatwg-fetch';
import ShowChildComponent from './ShowChildComponent';
class ChildComponent extends Component {
constructor(props) {
super(props);
this.fetchData = this.fetchData.bind(this);
const tempmyObject = {};
this.state = {
myObject: tempmyObject
};
}
fetchData(event) {
const that = this;
fetch(this.props.path)
.....
that.setState(
{
myObject: myObject,
});
....
event.preventDefault();
};
render() {
return (
<div>
<ShowChildComponent
fields={this.props.arrayOfObjects}
myObject={this.state.myObject}
buttonName={this.props.name}
onChange={this.fetchData}
/>
</div>
);
}
}
export default ChildComponent;
and I could simulate a click. My tests looked like this:
it('renders ChildComponent button click message', () => {
const wrapper = mount(<ChildComponent fields={arrayOfObjects}
myObject={myObject}
path={'/some/path'}
buttonName='foo'
/>);
const p = <p className='myParagraph' id={id}>{value}</p>;
wrapper.find('button.getData').simulate('click');
expect(wrapper.find(ShowChildComponent).containsMatchingElement(p)).toEqual(true);
});
How do I achieve the same thing after I refactored?
I am working on an app which was created with the Vue loader's webpack template.
I included testing with Karma as an option when creating the project, so it was all set up and I haven't changed any of the config.
The app is a Github user lookup which currently consists of three components; App.vue, Stats.vue and UserForm.vue. The stats and form components are children of the containing app component.
Here is App.vue:
<template>
<div id="app">
<user-form
v-model="inputValue"
#go="submit"
:input-value="inputValue"
></user-form>
<stats
:username="username"
:avatar="avatar"
:fave-lang="faveLang"
:followers="followers"
></stats>
</div>
</template>
<script>
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import _ from 'lodash'
import UserForm from './components/UserForm'
import Stats from './components/Stats'
Vue.use(VueAxios, axios)
export default {
name: 'app',
components: {
UserForm,
Stats
},
data () {
return {
inputValue: '',
username: '',
avatar: '',
followers: [],
faveLang: '',
urlBase: 'https://api.github.com/users'
}
},
methods: {
submit () {
if (this.inputValue) {
const api = `${this.urlBase}/${this.inputValue}`
this.fetchUser(api)
}
},
fetchUser (api) {
Vue.axios.get(api).then((response) => {
const { data } = response
this.inputValue = ''
this.username = data.login
this.avatar = data.avatar_url
this.fetchFollowers()
this.fetchFaveLang()
}).catch(error => {
console.warn('ERROR:', error)
})
},
fetchFollowers () {
Vue.axios.get(`${this.urlBase}/${this.username}/followers`).then(followersResponse => {
this.followers = followersResponse.data.map(follower => {
return follower.login
})
})
},
fetchFaveLang () {
Vue.axios.get(`${this.urlBase}/${this.username}/repos`).then(reposResponse => {
const langs = reposResponse.data.map(repo => {
return repo.language
})
// Get most commonly occurring string from array
const faveLang = _.chain(langs).countBy().toPairs().maxBy(_.last).head().value()
if (faveLang !== 'null') {
this.faveLang = faveLang
} else {
this.faveLang = ''
}
})
}
}
}
</script>
<style lang="stylus">
body
background-color goldenrod
</style>
Here is Stats.vue:
<template>
<div class="container">
<h1 class="username" v-if="username">{{username}}</h1>
<img v-if="avatar" :src="avatar" class="avatar">
<h2 v-if="faveLang">Favourite Language: {{faveLang}}</h2>
<h3 v-if="followers.length > 0">Followers ({{followers.length}}):</h3>
<ul v-if="followers.length > 0">
<li v-for="follower in followers">
{{follower}}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'stats',
props: [
'username',
'avatar',
'faveLang',
'followers'
]
}
</script>
<style lang="stylus" scoped>
h1
font-size 44px
.avatar
height 200px
width 200px
border-radius 10%
.container
display flex
align-items center
flex-flow column
font-family Comic Sans MS
</style>
And here is UserForm.vue:
<template>
<form #submit.prevent="handleSubmit">
<input
class="input"
:value="inputValue"
#input="updateValue($event.target.value)"
type="text"
placeholder="Enter a GitHub username..."
>
<button class="button">Go!</button>
</form>
</template>
<script>
export default {
props: ['inputValue'],
name: 'user-form',
methods: {
updateValue (value) {
this.$emit('input', value)
},
handleSubmit () {
this.$emit('go')
}
}
}
</script>
<style lang="stylus" scoped>
input
width 320px
input,
button
font-size 25px
form
display flex
justify-content center
</style>
I wrote a trivial test for UserForm.vue which test's the outerHTML of the <button>:
import Vue from 'vue'
import UserForm from 'src/components/UserForm'
describe('UserForm.vue', () => {
it('should have a data-attribute in the button outerHTML', () => {
const vm = new Vue({
el: document.createElement('div'),
render: (h) => h(UserForm)
})
expect(vm.$el.querySelector('.button').outerHTML)
.to.include('data-v')
})
})
This works fine; the output when running npm run unit is:
UserForm.vue
✓ should have a data-attribute in the button outerHTML
However, when I tried to write a similarly simple test for Stats.vue based on the documentation, I ran into a problem.
Here is the test:
import Vue from 'vue'
import Stats from 'src/components/Stats'
// Inspect the generated HTML after a state update
it('updates the rendered message when vm.message updates', done => {
const vm = new Vue(Stats).$mount()
vm.username = 'foo'
// wait a "tick" after state change before asserting DOM updates
Vue.nextTick(() => {
expect(vm.$el.querySelector('.username').textContent).toBe('foo')
done()
})
})
and here is the respective error when running npm run unit:
ERROR LOG: '[Vue warn]: Error when rendering root instance: '
✗ updates the rendered message when vm.message updates
undefined is not an object (evaluating '_vm.followers.length')
I have tried the following in an attempt to get the test working:
Change how the vm is created in the Stats test to be the same as the UserForm test - same error is returned
Test individual parts of the component, for example the textContent of a div in the component - same error is returned
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front? How can I get around this issue to be able to successfully test my component?
(Repo with all code: https://github.com/alanbuchanan/vue-github-lookup-2)
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front?
This piece of code is from the render function that Vue compiled your template into. _vm is a placeholder that gets inserted automatically into all Javascript expressions when vue-loader converts the template into a render function during build - it does that to provide access to the component.
When you do this in your template:
{{followers.length}}
The compiled result in the render function for this piece of code will be:
_vm.followers.length
Now, why does the error happen in the first place? Because you have defined a prop followers on your component, but don't provide any data for it - therefore, the prop's value is undefined
Solution: either you provide a default value for the prop:
// Stats.vue
props: {
followers: { default: () => [] }, // function required to return fresh object
// ... other props
}
Or you propvide acual values for the prop:
// in the test:
const vm = new Vue({
...Stats,
propsData: {
followers: [/* ... actual data*/]
}
}).$mount()