I've built simple ErrorBoundary component for my project in Vue.js and I'm struggling to write unit test for it. Component's code below:
<template>
<div class="overvue-error-boundary">
<slot v-if="!error" />
<div class="error-message" v-else>Something went horribly wrong here.</div>
</div>
</template>
<script>
export default {
data () {
return {
error: false
}
},
errorCaptured (error, vm, info) {
this.error = true;
}
}
</script>
I've created an ErrorThrowingComponent that throws an error on created() lifecycle hook so I can test ErrorBoundary:
const ErrorThrowingComponent = Vue.component('error-throwing-component', {
created() {
throw new Error(`Generic error`);
},
render (h) {
return h('div', 'lorem ipsum')
}
});
describe('when component in slot throws an error', () => {
it('renders div.error-message', () => {
// this is when error is when 'Generic error' is thrown by ErrorThrowingComponent
const wrapper = shallowMount(OvervueErrorBoundary, {
slots: {
default: ErrorThrowingComponent
}});
// below code is not executed
expect(wrapper.contains(ErrorThrowingComponent)).to.be.false;
expect(wrapper.contains('div.error-message')).to.be.true;
});
});
The problem is that ErrorThrowingComponent throws an error when I'm trying to actually mount it (thus failing entire test). Is there any way I can prevent this from happening?
EDIT: What I'm trying to achieve is to actually mount the ErrorThrowing component in a default slot of ErrorBoundary component to assert if ErrorBoundary will render error message and not the slot. This is way I created the ErrorThrowingComponent in the first place. But I cannot assert ErrorBoundary's behavior, because I get an error when trying to create a wraper.
For anyone comming here with a similar problem: I've raised this on Vue Land's #vue-testing channel on Discord, and they suggested to move entire error-handling logic to a function which will be called from the errorCaptured() hook, and then just test this function. This approach seems sensible to me, so I decided to post it here.
Refactored ErrorBoundary component:
<template>
<div class="error-boundary">
<slot v-if="!error" />
<div class="error-message" v-else>Something went horribly wrong here. Error: {{ error.message }}</div>
</div>
</template>
<script>
export default {
data () {
return {
error: null
}
},
methods: {
interceptError(error) {
this.error = error;
}
},
errorCaptured (error, vm, info) {
this.interceptError(error);
}
}
</script>
Unit test using vue-test-utils:
describe('when interceptError method is called', () => {
it('renders div.error-message', () => {
const wrapper = shallowMount(OvervueErrorBoundary);
wrapper.vm.interceptError(new Error('Generic error'));
expect(wrapper.contains('div.error-message')).to.be.true;
});
});
Related
I'm trying to test my component that has the following conditional rendering:
const MyComponent = () => {
const [isVisible, setIsVisible] = (false);
if(selectedOption == 'optionOne')
setIsVisible(true);
else
setIsVisible(false);
return (
<div>
<Select data-testid="select1" selectedOption={selectedOption} />
{isVisible ? <Select data-testid="select2" selectedOption={anotherSelectedOption} /> : null }
</div>
)}
If selectedOption in select1 is 'optionOne', then select2 shows up.
Here is how I am testing it:
describe('Testing', () => {
let container: ElementWrapper<HTMLElement>;
const testState = {
userChoice1: {
selectedOption: ['optionOne'],
},
userChoice2: {
selectedOption: ['test1', 'test2'],
},
} as AppState;
beforeEach(() => {
container = render(<MyComponent/>, testState);
});
it('should show select2 if optionOne is selected', async () => {
const { getByTestId, getAllByTestId } = render(<MyComponent/>);
expect(container.find('span').getElement().textContent).toBe("optionOne"); // this successfully finds select1 with optionOne selected, all good
await screen.findAllByTestId('select2')
expect(screen.getAllByTestId('select2')).toBeInTheDocument();
});
In the testing above, as select1 has optionOne selected, I expect to select2 to show up. However, I am getting an error Unable to find an element by: [data-testid="select2"]. It also returns the whole HTML body, where I see select1 element with optionOne selected, but no select2 at all as it seems to still be hidden.
What am I missing here? How can I unhide select2 within the unit test?
I use bootstrap-vue for the vue.js css framework and decided to test the desired component. This component uses b-table and has a v-slot with a call function.
<template>
<b-table
striped
bordered
:items="items"
:fields="$t('pages.events.show.users.fields')"
>
<template v-slot:cell(name)="{ item }">
<b-avatar :src="item.avatar" class="mr-2" />
<span v-text="item.name" />
</template>
</b-table>
</template>
and I'm writing a simple test for this component:
import { shallowMount } from "#vue/test-utils";
import EventUsersTable from "./EventUsersTable.vue";
/* #region Test setup */
const factory = () => {
return shallowMount(EventUsersTable, {
mocks: {
$t: jest.fn()
},
stubs: {
BTable: true
}
});
};
/* #endregion */
describe("EventUsersTable.vue", () => {
let wrapper;
beforeEach(() => (wrapper = factory()));
test("should render component", () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
and i have error with this content: [Vue warn]: Error in render: "TypeError: Cannot read property 'item' of undefined"
for write test for this component i need fix this problem.
And I have a problem with the vue unit test document, they are very limited and with few examples.
If anyone knows a source that has more examples and scenarios for vue language tests, thank you for introducing it.
After inquiring, I came up with a solution that, using mount and adding the main component, was able to solve my problem.
import { mount } from "#vue/test-utils";
import { BTable } from "bootstrap-vue";
import EventUsersTable from "./EventUsersTable.vue";
/* #region Test setup */
const factory = () => {
return mount(EventUsersTable, {
mocks: {
$t: jest.fn()
},
stubs: {
BTable
}
});
};
/* #endregion */
describe("EventUsersTable.vue", () => {
let wrapper;
beforeEach(() => (wrapper = factory()));
// FIXME: fix this bug for render component
test("should render component", () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
I have created a Vue component and a unit test to validate its behavior. I can use the component on my Vue app without any issues, but when I run my unit test using jest, I get the error:
error TS2339: Property 'extend' does not exist on type 'typ
eof import("</path/to/module>")'.
1 export default Vue.extend({
vButton.ts - Component under test
export default Vue.extend({
template: `
<div>
<button class="default-button" #click="click">
<span>{{ text }}</span>
</button>
</div>
`,
props: {
text: String,
action: Function
},
methods: {
click(): void {
this.$emit('action');
}
}
});
vButton.spec.ts - unit test
import { mount } from '#vue/test-utils'
import vButton from '../views/core/ts/components/vButton'
describe('vButton', () => {
describe(':props', () => {
it(':text - should render a button with the passed-in label text', () => {
const msg = 'new message';
const wrapper = mount(vButton, {
propsData: { text: msg },
});
expect(wrapper.text()).toMatch(msg);
});
});
describe('#events', () => {
it('#click - should emit an "action" event when the button is clicked', () => {
const wrapper = mount(vButton);
const button = wrapper.find('button')
button.trigger('click')
expect(wrapper.emitted().action).toBeTruthy()
})
});
});
I expect Vue.extend to work fine when running the unit test.
Project: https://github.com/marioedgar/webpack-unit-test
I have a Vue.js app I generated with the vue CLI. I've only edited the HelloWorld component slightly to fetch some async data from my test service, you can see that here:
<template>
<h1>{{ message }}</h1>
</template>
<script>
import service from './test.service'
export default {
name: 'HelloWorld',
created () {
service.getMessage().then(message => {
this.message = message
})
},
data () {
return {
message: 'A'
}
}
}
</script>
<style scoped>
</style>
The test service lives in the same directory and is very simple:
class Service {
getMessage () {
return new Promise((resolve, reject) => {
console.log('hello from test service')
resolve('B')
})
}
}
const service = new Service()
export default service
So in order to mock this service, im using the webpack vue-loader to inject the mock service as described in the official documentation here:
https://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html
So here is my test which is almost identical to the example:
import Vue from 'vue'
const HelloInjector = require('!!vue-loader?inject!../../../src/components/HelloWorld')
const Hello = HelloInjector({
// mock it
'./test.service': {
getMessage () {
return new Promise((resolve, reject) => {
resolve('C')
})
}
}
})
describe('HelloWorld.vue', () => {
it('should render', () => {
const vm = new Vue({
template: '<div><test></test></div>',
components: {
'test': Hello
}
}).$mount()
expect(vm.$el.querySelector('h1').textContent).to.equal('C')
})
})
There are two issues i am facing:
The test fails because the assertion is executing before the mocked promise is resolved. From my understanding, this is because the vue lifecycle hasnt completely finished when im doing my asserting. The common patter to wait for the next cycle would be to wrapp my assertion around the next tick function like this:
it('should render', (done) => {
const vm = new Vue({
template: '<div><test></test></div>',
components: {
'test': Hello
}
}).$mount()
Vue.nextTick(() => {
expect(vm.$el.querySelector('h1').textContent).to.equal('C')
done()
})
})
This, however, does not work unless I nest 3 nextTicks, which seems extremely hacky to me. Is there something I am missing to get this to work? this example seems extremely straightforward, but I cannot get this test to pass without lots of nextTicks
I keep getting a strange error, intermittently... this warning shows up probably 50% of the time and is not consistant at all.
[vue warn] Failed to mount component: template or render function is not defined
Again, this happens only sometimes. I can run the same exact unit test without any changes and it will show me this message 50% of the time.
I truly couldn't figure out why sometimes the component failed to mount. I'm not even sure it's related to the injector but, in any case, I kept the test consistent by not using it; trying a different approach instead.
The component might be more testable if the service is injected through the props instead of being directly used.
<template>
<div>
<h1>{{ message }}</h1>
</div>
</template>
<script>
import service from './test.service'
export default {
name: 'HelloWorld',
created () {
this.service.getMessage().then(message => {
this.message = message
})
},
data () {
return {
message: 'A'
}
},
props: {
service: {
default: service
}
}
}
</script>
<style scoped>
</style>
This makes the injector unnecessary as the mocked service can be instead passed to the component using propsData in the constructor.
import Vue from 'vue'
import Async from '#/components/Async'
const service = {
getMessage () {
return new Promise((resolve, reject) => {
resolve('C')
})
}
}
describe('Async.vue', () => {
let vm
before(() => {
const Constructor = Vue.extend(Async)
vm = new Constructor({
propsData: {
service: service
}
}).$mount()
})
it('should render', function () {
// Wrapping the tick inside a promise, bypassing PhantomJS's lack of support
return (new Promise(resolve => Vue.nextTick(() => resolve()))).then(() => {
expect(vm.$el.querySelector('h1').textContent).to.equal('C')
})
})
})
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()