test Vue js props component with Vue-test-utils - vue.js

I'm new on testing Vue apps, I'm trying to test props in one Vue component, using Vue-test-utils package. I'm wondering if I'm creating the propsData is the proper way or there is another approach which in that case it's better to test this component successfully
Template.spec.js
import { mount } from '#vue/test-utils';
import Template from '~/components/Template.vue';
describe('Template', () => {
const wrapper = mount(Template, {
propsData: {
image: 'https://loremflickr.com/640/480/dog',
location: 'London',
jobPosition: 'Developer',
hashtags: ['es6', 'vue']
}
});
it('should render member props', () => {
expect(wrapper.props().image).toMatch('https://loremflickr.com/640/480/dog');
expect(wrapper.props().location).toMatch('London');
expect(wrapper.props().jobPosition).toMatch('Developer');
expect(wrapper.props().hashtags).toEqual(['es6', 'vue']);
});
});
Template.vue
<template>
<div class="template">
<img
:src="image"
>
<span>{{ location }}</span>
<span>{{ jobPosition }}</span>
</div>
</template>
<script>
export default {
name: 'template',
props: {
image: {
type: String,
required: true
},
location: {
type: String,
required: true
},
jobPosition: {
type: String,
required: true
},
}
};
</script>

You can test not props value, but component's behavior depending on props set. For example, your component can set some classes or show some element if some prop is set
e.g.
describe( 'BaseButton.vue icon rendering', () => {
const icon = 'laptop';
const wrapper = shallowMount( BaseButton, {
propsData : {
icon : icon,
},
} );
const wrapperWithoutIcon = shallowMount( BaseButton );
it( 'renders icon component', () => {
expect( wrapper.contains( FontAwesomeIcon ) ).toBe( true );
} );
it( 'sets a right classname', () => {
expect( wrapper.classes() ).toContain('--is-iconed');
} );
it( 'doesn\'t render an icon when icon prop is not passed', () => {
expect( wrapperWithoutIcon.contains( FontAwesomeIcon ) ).toBe( false );
} );
it( 'sets right prop for icon component', () => {
const iconComponent = wrapper.find( FontAwesomeIcon );
expect( iconComponent.attributes('icon') ).toMatch( icon );
} );
} );

Your Template.spec.js is fine, in which you set up your fake props.
You can check if those fake props are rendered out into the HTML.
Here is an example:
it('title render properly thru passed prop', () => {
const wrapper = shallowMount(app, {
propsData: {
data: {
title: 'here are the title'
}
},
})
expect(wrapper.text()).toContain('here are the title')
})
This way, you are checking if your code can render your props to HTML

Related

Props arguments are not reactive in setup

I'm trying to develop a component that compiles the given html; I succeeded with constant html texts, but I can't make this work with changing html texts.
main.js
app.component("dyno-html", {
props: ["htmlTxt"],
setup(props) {
watchEffect(() => {
console.log(`htmlText is: ` + props.htmlTxt);
return compile(props.htmlTxt);
});
return compile(props.htmlTxt);
},
});
Home.js
<template>
<div class="home">
<dyno-html
:htmlTxt="html2"
:bound="myBoundVar"
#buttonclick="onClick"
></dyno-html>
-------------
<dyno-html
:htmlTxt="html"
:bound="myBoundVar"
#buttonclick="onClick"
></dyno-html>
</div>
</template>
<script>
export default {
name: "Home",
components: {},
data: function() {
return {
html: "",
html2: `<div> Static! <button #click="$emit('buttonclick', $event)">CLICK ME</button></div>`
};
},
mounted() {
// get the html from somewhere...
setTimeout(() => {
this.html = `
<div>
Dynamic!
<button #click="$emit('buttonclick', $event)">CLICK ME</button>
</div>
`;
}, 1000);
},
methods: {
onClick(ev) {
console.log(ev);
console.log("You clicked me!");
this.html2 = "<b>Bye Bye!</b>";
},
},
};
</script>
Outcome:
Console:
It seems the changes of htmlText arrives to setup function, but it doesn't affect the compile function!
This is the expected behaviour because prop value is read once and results in static render function.
Prop value should be read inside render function. It can be wrapped with a computed to avoid unneeded compiler calls:
const textCompRef = computed(() => ({ render: compile(props.htmlTxt) }));
return () => h(textCompRef.value);

Jest + Coverage + VueJs how to cover vue methods?

I was trying to cover the codes to increase the coverage
report percentage,
How to cover the if statement inside vue methods?
In my case using #vue/test-utils:"^1.1.4" and vue: "^2.6.12" package version, FYI, And below is my actual component,
<template>
<div :class="iconcls" >
<el-image
ref='cal-modal'
class="icons"
#click="handleRedirectRouter(urlname)"
:src="require(`#/assets/designsystem/home/${iconurl}`)"
fit="fill" />
<div class="desc" >{{ icondesc }}</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
#Component({
components: {}
})
class IconHolder extends Vue {
#Prop({ default: "" }) iconcls!: any;
#Prop({ default: "" }) iconurl!: any;
#Prop({ default: "" }) icondesc!: any;
#Prop({ default: "" }) urlname!: any;
handleRedirectRouter(url: string) {
if (url !== "") {
this.$router.push({ name: url });
}
}
}
export default IconHolder;
</script>
Coverage Report for Iconholder.vue component
EDIT 2 : Ater #tony updation,
i have tried with this below test suties but still getting errors,
import Vue from "vue";
import Vuex from "vuex";
import IconHolder from '#/components/designsystem/Home/IconHolder.vue';
import ElementUI, { Image } from "element-ui";
import { mount, createLocalVue } from '#vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(ElementUI, {
Image
});
Vue.component('el-image', Image);
describe("IconHolder.spec.vue", () => {
it('pushes route by name', async () => {
const push = jest.fn();
const wrapper = mount(IconHolder, {
propsData: {
iconcls:"dshomesec5_comp_icons",
icondesc:"about",
iconurl:"components_icn_15.svg",
urlname: 'about'
},
mocks: {
$router: {
push
}
}
})
await wrapper.findComponent({ name: 'el-image' }).trigger('click');
expect(push).toHaveBeenCalledWith({ name: 'about' });
})
})
ERROR REPORT:
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: {"name": "about"}
Number of calls: 0
30 | })
31 | await wrapper.findComponent({ name: 'el-image' }).trigger('click');
> 32 | expect(push).toHaveBeenCalledWith({ name: 'about' })
Create a unit test that runs that method with a non-empty string for url.
Mount the component with an initial non-empty urlname prop.
Mock the $router.push method with jest.fn(), which we'll use to verify the call later.
Find the el-image component that is bound to that method (as a click handler).
Trigger the click event on that component.
Verify $router.push was called with the specified urlname.
it('pushes route by name', () => {
/* 2 */
const push = jest.fn()
const wrapper = shallowMount(IconHolder, {
/* 1 */
propsData: {
urlname: 'about'
},
/* 2 */
mocks: {
$router: {
push
}
}
})
/* 3 👇*/ /* 4 👇*/
await wrapper.findComponent({ name: 'el-image' }).trigger('click')
/* 5 */
expect(push).toHaveBeenCalledWith({ name: 'about' })
})

Cannot find data-testid attribute in Vue Component with Jest

I am trying to build a test that will target an element with the data-testid attribute. I have a BaseTile component that looks like this:
<template>
<div
data-testid="base-tile-icon"
v-if="!!this.$slots.icon"
>
<slot name="icon"></slot>
</div>
</template>
<script>
export default {};
</script>
<style></style>
And my test looks like this:
import { mount } from '#vue/test-utils';
import BaseTile from '#/components/BaseTile';
const factory = (slot = 'default') => {
return mount(BaseTile, {
slots: {
[slot]: '<div class="test-msg"></div>'
}
});
};
it('has an icon slot if an icon is provided', () => {
let wrapper = factory({ slot: 'icon' });
const input = wrapper.find('[data-testid="base-tile-icon"]');
expect(input.findAll('.test-msg').length).toBe(1);
});
How do I appropriately target the data-testid attribute with this test?
The named parameter of the factory is implemented incorrectly. The correct method is described in this post: Is there a way to provide named parameters in a function call in JavaScript?
The correct way to implement this is as follows:
import { mount } from '#vue/test-utils';
import BaseTile from '#/components/BaseTile';
const factory = ({ slot = 'default' } = {}) => {
return mount(BaseTile, {
slots: {
[slot]: '<div class="test-msg"></div>'
}
});
};
it('has an icon slot if an icon is provided', () => {
let wrapper = factory({ slot: 'icon' });
const input = wrapper.find('[data-testid="base-tile-icon"]');
expect(input.findAll('.test-msg').length).toBe(1);
});

Detect vuex state change to execute a method inside a nuxt layout

I am trying to show vuetify snackbar alert, once I completed a form submission inside a page or vue component. I use vuex store to manage alert type and message.
my-nuxt-app/store/alerts.js
export const state = () => ({
message: '',
type: ''
});
export const getters = {
hasAlert(state) {
return state.message !== '';
},
alertMessage(state) {
return state.message;
},
alertType(state) {
return state.type;
}
};
export const mutations = {
SET_ALERT(state, payload) {
state.type = payload.type;
state.message = payload.message;
}
};
export const actions = {
setAlert({commit}, payload) {
commit('SET_ALERT', payload);
},
clearAlert({commit}) {
commit('SET_ALERT', {});
}
};
And I created a nuxt plugin to access getters globally in my application.
my-nuxt-app/plugins/alert.js
import Vue from 'vue';
import {mapGetters} from 'vuex';
const Alert = {
install(Vue, options) {
Vue.mixin({
computed: {
...mapGetters({
hasAlert: 'alerts/hasAlert',
alertType: 'alerts/alertType',
alertMessage: 'alerts/alertMessage'
})
}
});
}
};
Vue.use(Alert);
Inside my AccountForm component submit method, I am dispatching my alert information to store like below.
my-nuxt-app/components/form/AccountForm.vue
...
methods: {
async submit () {
try {
await this.$axios.patch("/settings/profile", this.form);
this.$store.dispatch('alerts/setAlert', {
type: 'success',
message: 'You have successfully updated your information.'
});
} catch (e) {
}
}
},
...
}
...
And this AccountForm.vue component is a child component of profile.vue page which is obviously inside the pages folder of my project. And also I have extended the dashboard.vue layout to this profile.vue page and to the most of the pages inside my pages directory as a common layout. Hence, I added the snackbar component into dashboard layout to show a alert message whenever required.
my-nuxt-app/layouts/dashboard.vue
<template>
...
<v-snackbar
:timeout="snackbar.timeout"
:color="snackbar.color"
:top="snackbar.y === 'top'"
:bottom="snackbar.y === 'bottom'"
:right="snackbar.x === 'right'"
:left="snackbar.x === 'left'"
:multi-line="snackbar.mode === 'multi-line'"
:vertical="snackbar.mode === 'vertical'"
v-model="snackbar.show"
>
{{ snackbar.text }}
<v-btn flat icon dark #click.native="snackbar.show = false">
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
...
</template>
<script>
...
data: () => ({
snackbar: {
show: false,
y: 'top',
x: null,
mode: '',
timeout: 6000,
color: '',
text: ''
},
}),
computed: {
availableAlert: function () {
return this.hasAlert;
}
},
watch: {
availableAlert: function(alert) {
if(alert) {
this.showAlert(this.alertType, this.alertMessage);
this.$store.dispatch('alerts/clearAlert');
}
}
},
methods: {
showAlert(type, message) {
this.snackbar.show = true;
this.snackbar.color = type;
this.snackbar.text = message;
}
}
</script>
I am getting the alert message for the first time submission of the form and after that I have to reload the page and then submit to get the alert. Please enlighten me a way to detect the vuex state change and trigger showAlert method inside the dashboard.vue accordingly.
It's most likely the way you're checking hasAlert
Your clearAlert passes an empty object, your setAlert is trying to assign properties of that empty object, while your hasAlert is checking if it's an empty string.
If you change your clearAlert to:
clearAlert({commit}) {
commit('SET_ALERT', { message: '', type: '' });
}
That should fix your issue.

How to use a component multiple times while passing different data to it?

I'm trying to create a snackbar component for showing simple notifications. It can be used at many places in the entire application as well as on a single page as well. I've created a component as child component and imported it in the parent component where i want to use it. In this parent component many times this child can be used. How should i implement in a way that each time this component is called it gets its appropriate data(Ex. for error color=red text="error", for success color="green" message="success).
Any suggestions on how to implement it?
parent.vue----------------------------
<snackbar
:snackbar="snackbar"
:color="color"
:text="message"
v-on:requestClose="close"
/>
data() {
return {
snackbar: false,
color: "orange",
timeout: 3000,
message: "calling from employee compoenent"
};
},
methods: {
hello() {
console.log("button clicked!!!");
this.snackbar = true;
},
close() {
this.snackbar = false;
},
child.vue-----------------------------------------------
<template>
<v-snackbar v-model="snackbar" right top :timeout="timeout" :color="color"
>{{ text }}
<v-btn dark text #click.native="$emit('requestClose')">Close</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: "snackbar",
data() {
return {
timeout: 3000
};
},
props: ["snackbar", "text", "color"],
};
</script>
<style></style>
Recommended would be to create a custom wrapper Vue plugin
plugins/snackbar/index.js
import snackbar from './snackbar.vue'
export default {
install (Vue) {
// INSTALL
if (this.installed) return
this.installed = true
// RENDER
const root = new Vue({ render: h => h(snackbar) })
root.$mount(document.body.appendChild(document.createElement('div')))
// APIs
let apis = Vue.prototype['$snackbar'] = {
show: ({ text="Foo", color="blue" }) => root.$emit('show', { text, color }), // SHOW
hide: () => root.$emit('hide') // HIDE
}
Vue.prototype['$snackbar'] = apis
Vue.snackbar = apis
}
}
plugins/snackbar/snackbar.vue
<template>
<v-snackbar right top v-model="show" :timeout="timeout" :color="color">
{{ text }}
<v-btn dark text #click.native="this.show = false">Close</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: "snackbar",
data() {
return {
show,
timeout: 3000,
text: "",
color: ""
};
},
mounted () {
// LISTENING :: SHOW
this.$root.$on('show', ({ text, color }) => {
this.text = text
this.color = color
this.show = true
})
// LISTENING :: HIDE
this.$root.$on('hide', () => this.show = false)
}
};
</script>
// main.js
import Snackbar from './plugins/snackbar/index.js'
Vue.use(Snackbar)
To show / hide it in any component
this.$snackbar.show({ text: "Foo bar", color: "red" }) // OR
Vue.snackbar.show({ text: "Foo bar", color: "red" })
As per use case, you can keep updating your plugin with more params / APIs.
Alternative: By using an event bus
event-bus/bus.js
// Create an event bus
import Vue from 'vue'
export default new Vue()
app.vue
<template>
// Render the component in app.vue
<v-snackbar
right top
v-model="snackbar.show"
:timeout="snackbar.timeout"
:color="snackbar.color"
>
{{ snackbar.text }}
<v-btn
dark text
#click.native="this.snackbar.show = false"
>
Close
</v-btn>
</v-snackbar>
</template>
<script>
import bus from './event-bus/bus.js'
export default {
data () {
return {
snackbar: {
show: false,
text: '',
color: '',
timeout: 3000
}
}
},
mounted () {
// LISTEN TO SHOW
bus.$on('show', ({ text, color }) => {
this.snackbar.text = 'foo'
this.snackbar.color = 'red'
this.snackbar.show = true
})
// LISTEN TO HIDE
bus.$on('hide', () => this.snackbar.show = false)
}
}
</script>
To show / hide snackbar from any component
import bus from './event-bus/bus.js
export default {
mounted () {
bus.emit('show', { text: 'Foo bar baz', color: 'orange' }) // TO SHOW
// bus.emit('hide') // TO HIDE
}
}
Another way: By using Vuex
Render the <v-snackbar> in app.vue as done in an alternative approach & use Vuex state / getters to pass the value to the props of v-snackbar.
I did it by using combination of global components and Vuex. The answer is a bit lengthy because I provide example along the description, please bear with me :)
I first create a snackbar store with color and text as its state and a setSnackbar() action which receives color and text as params. Then you can create your Snackbar component and don't forget to have your getters, actions mapped into it. Some code snippet:
// snackbar component
<template>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="6000" bottom right>
{{ snackbar.text }}
<v-btn dark text #click="snackbarClosed()">Close</v-btn>
</v-snackbar>
</template>
<script lang="ts">
import Vue from "vue";
import { mapGetters, mapActions } from "vuex";
export default Vue.extend({
computed: {
...mapGetters(["snackbar"])
},
methods: {
snackbarClosed() {
this.resetSnackbar();
},
...mapActions(["resetSnackbar"])
}
});
</script>
// snackbar store
const state = {
snackbar: {
show: false,
text: '',
color: ''
}
};
const getters = {
snackbar: (state: any) => state.snackbar
};
const actions = {
async setSnackbar({ commit }, params) {
commit('updateSnackbar', Object.assign({}, { show: true }, params))
},
async resetSnackbar({ commit }) {
const setting: SnackbarSetting = {
show: false,
text: '',
color: ''
};
commit('updateSnackbar', setting)
};
const mutations = {
updateSnackbar: (state: any, snackbar: SnackbarSetting) => {
state.show = snackbar.show;
state.text = snackbar.text;
state.color = snackbar.color;
}
};
To make Snackbar component globally available, import your Snackbar component into your main.ts and add the line Vue.component('Snackbar', Snackbar); before new Vue. Its purpose is to register your Snackbar component globally before initializing the Vue instance. Example:
// main.ts
import Snackbar from './components/Snackbar.vue';
Vue.component('Snackbar', Snackbar);
new Vue({
...
Before you want to display your snackbar in the app, by my recommendation you should place <Snackbar /> in your App.vue so that the snackbar can appear before your components and you won't be facing missing snackbar when changing between components.
When you want to display your snackbar, just do this in your component:
// any of your component
methods: {
someEvent() {
this.someApiCall({
// some data passing
}).then(() => {
this.setSnackbar({
text: 'Data has been updated.',
color: 'success'
});
}).catch(() => {
this.setSnackbar({
text: 'Failed to update data.',
color: 'error'
});
});
},
...mapActions(['setSnackbar'])
}
Hope you can work it out, please do not hesitate to let me know if you need something. Here's some extra material for you: Global component registration
you can watch for props in child this'll make color changes when any change happen in parent:
watch: {
color: function(value) {
"add color value to your dom css class"
}
}