I am writing unit test for an existing vue component. The component has a method which runs on mount. The method uses return this.$parent.$parent.someData. I am looking for a way to pass grandparent mock data to the component while writing unit test. I know the way to pass parent component as mock, but looking for a way to pass grand parent.
I am using the following code in my test
const wrapper = shallowMount(MyComponent,{
parentComponent:ParentComponent
})
Looking for a way to pass grandparent component. I am using vue-test-utils.
PS: I cannot make changes in existing code
This is the most straight-forward way:
import Child from './path/to/Child.vue'
const GP = shalowMount({
template: '<div />',
data: () => ({
foo: 'bar' // mocked data
})
})
const wrapper = shallowMount(Child, {
parentComponent: {
created() {
this.$parent = GP.vm
}
}
})
console.log(wrapper.vm.$parent.$parent.foo) // bar
If you want to test closer to what the user gets in the browser, you could shallowMount the actual ancestors (you'll need their deps in this case: stores, third-party packages, etc...):
import GrandParent from './path/to/GrandParent.vue'
import Parent from './path/to/Parent.vue'
import Child from './path/to/Child.vue'
const GP = shallowMount(GrandParent);
/* set GP data, props, mocks or spies */
const wrapper = shallowMount(Child, {
parentComponent: shallowMount(Parent, {
parentComponent: GP.vm
}).vm
})
Related
I have a Vue page like this:
<template>
</template>
<script>
created(){
this.doSomething();
}
methods: {
doSomething() {
.....
}
}
</script>
Now, we want to the testing of this created hook and check that doSomething() method is called.
Tried like this, jest is also imported in package.json
import {
shallowMount,
createLocalVue,
} from '#vue/test-utils';
const localVue = createLocalVue();
import Xyx from '/Xyx.vue';
const init = () => {
wrapper = shallowMount(Xyx, { localVue });
cmp = wrapper.vm;
};
describe('#created', () => {
it('#doSomething', () => {
init();
wrapper.setMethods({
doSomething: jest.fn(),
})
expect(cmp.doSomething).toHaveBeenCalled();
});
});
Can I do the unit test case of this created hook?
The methods option was deprecated in v1 of #vue/test-utils, so the accepted answer no longer works. I ran into this issue myself and decided to dig into the source to figure out how I could test this.
It looks like Vue actually stores all the hooks in the $options property. Each hook has an option that is an array of functions. It is important to note that a context is not bound to said functions, so you will need to use call or apply to execute them.
vm.$options.created.forEach(hook => {
hook.call(vm);
});
Because your method is called on created, it is run before you are setting the mock. Therefore, your test will fail.
You have to replace the method with the mock on initialization (in your case, on shallowMount):
describe('Xyz', () => {
it('should call doSomething() when created', () => {
const doSomething = jest.fn()
wrapper = shallowMount(Xyz, {
localvue,
methods: { doSomething }
});
expect(doSomething).toHaveBeenCalled();
});
});
Sidenote: you're not declaring cmp. At the start of your test, you should have a let cmp;
A very similar discussion here. Above the linked comment there's a method to mock properties of most Vue component lifecycle hooks.
It's possible to call hooks when we need in our tests. For example if we need to mock some data before calling a hook.
import App from '#/App.vue';
// in test
App.created.call(wrapper.vm);
Also in Typescript if we use vue-property-decorator it changes the shape of component, so needs to be done like this:
App.extendOptions.created.call(wrapper.vm)
I started Vue.js two days ago and I already need your help.
I have a state machine component which renders a child component passed as parameter. Because I do not know in advance which component I am going to display and the props it takes, I am using render functions instead of templates for the state machine parent component.
Code excerpt goes like this :
function render(h) {
const app = this;
props.reduce((acc, key) => ((acc[key] = app[key]), acc), currentPropsObj);
console.log("child props", currentPropsObj);
return app.hasStarted
? h(
renderComponent,
{
// copy the props from the machine vue component to the render component
props: Object.assign({}, currentPropsObj)
},
[]
)
: h("div", {}, "LoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLoLo");
}
return Vue.component(name, {
render,
data: function() {
return initialData;
},
methods: {
set: function(stateObj) {
Object.keys(stateObj).forEach(key => (this[key] = stateObj[key]));
}
},
The issue is that the dynamic renderComponent does not seem to take into account the props it (should) receives as parameter. I checked that currentPropsObj is correctly computed. Using Vue devtool, I also checked that the Search child component (the one I am using in the demo) props are not udpated when its parent data is updated.
For a full demo and reproduction, I made a codesandbox : https://codesandbox.io/s/m5jzqw9w3y
What can I be doing wrong?
when I instantiate functional component using this code
const Component_Constructor = Vue.extend(Component);
let component_instance = new Component_Constructor();
component_instance.$mount();
the component gets undefined context argument on the render function
how can i pass parameters (props, slots, children, ...) to the component?
the only workaround I found so far is to wrap the functional component into another normal component like this:
let AComponent = {
functional: true,
name: 'a-component',
render(h, context) {
return h('div', context.children[0].text);
}
};
let template = `<a-component>test content</a-component>`;
let WrapperComponent = Vue.extend({
components: {AComponent},
template,
});
let componentInstance = new WrapperComponent().$mount();
let content = componentInstance.$el;
As one of the vuejs core team said here https://forum.vuejs.org/t/functional-component-with-vue-extend/40752, you can not mount functional component manually.
$mount() need a vue instance, that a functional component does not have.
I would like to implement this kind of logic that assign this.$store.value to local data.
This is how I do in pages/index.vue for instance.
method: {
this.value = this.$store.value
}
I want to write it down into mixins because I actually have another logics around it and I use some pages.
However, I don't know how should I access this(VueInstnce) from mixins?
It is not supported by Vue because mixin runs first before component's code,
then mixin is bound (merged) by Vue to the component instance so it's easy to access mixin from component/instance scope, but not vice versa.
To achieve your need I think the mixin method (like created) should be run (for example) with a given reference to the instance of your component as a parameter, but it's not like that.
However, if you reorganize your code to run what you need from instance.created
accessing there methods and data of mixin is possible and passing arguments on your own:
var mixin = {
data: {mixin: 'mixin'},
created: function () {
console.log('mixin hook called')
},
methods: { test: function(arg){console.log(arg); } }
};
vm=new Vue({
data: {component: 'component'},
mixins: [mixin],
created: function () {
console.log('called hook of ' + this.component + ' and accessing ' + this.mixin)
},
});
vm.test(vm.mixin);
vm.test(vm.component); // no problem to run mixin's method with component's data
> mixin hook called
> called hook of component and accessing mixin
> mixin
> component
Okay so I don't know if it's considered a bad practice but I have managed to accomplish one-way data transmission without an event bus.
I am using vuejs 3 with composition api. Requirement: all components shall be able to access a global singleton component shown on top of whole app.
plugin.js - here we use the created event in mixin to get a reference of an component instance. In example below I always have just 1 tracker component instance (global popup). If you have a different more complex scenario I would recommend sticking with event bus solution instead..
import Tracker from "#/plugins/ProgressTracker/components/Tracker.vue";
export default {
install(app, params = {}) {
app.component("tracker", Tracker);
let instance = undefined;
app.mixin({
created() {
if (this.$options.name === "ProgressTrackerPopup") {
instance = this;
}
},
});
const progressTracker = () => {
//
};
progressTracker.show = function () {
instance.show();
};
app.config.globalProperties.$progressTracker = progressTracker;
},
};
useProgressTracker.js - globally reusable composable function that exposes show method
import { ref, computed, getCurrentInstance } from "vue";
export default function useProgressTracker() {
const internalInstance = getCurrentInstance();
const progressTracker = internalInstance.appContext.config.globalProperties.$progressTracker;
const show = () => {
progressTracker.show();
};
return {
show,
};
}
Tracker.vue - component that we need to globally access from any other component (methods of it).. name is important. It shall be set in order for the mixin to be able to detect your component creation
<template>
<div class="onTop" v-if="isShow">test</div>
</template>
<script>
import { ref } from "vue";
export default {
name: "ProgressTrackerPopup",
setup() {
var isShow = ref(false);
const show = () => {
isShow.value = true;
};
return {
isShow,
show,
};
},
};
</script>
<style scoped>
.onTop{
position: absolute;
z-index: 1999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #0000004f;
}
</style>
this is it. Don't forget to register the plugin:
import ProgressTracker from "#/plugins/plugin.js";
// ..
app.use(ProgressTracker, {});
Now when you want the pop-up to be shown you invoke:
// <tracker />
import useProgressTracker from "#/plugins/ProgressTracker/use/useProgressTracker.js";
const tracker = useProgressTracker();
tracker.show();
The last line of code will basically invoke the show method on global component instance itself! Whereas if you used an event bus instead - you would have subscribed to the pop event on target component itself.
I find this solution to be useful when you don't want to deal with an event bus and the case is relatively trivial (you only have 1 global instance at all times). Though, you could of course use an array of instances and loop-invoke the methods on them in sequence.. :)
Am from Angular2 whereby i was used to services and injection of services hence reusing functions how do i achieve the same in vuejs
eg:
I would like to create only one function to set and retrieve localstorage data.
so am doing it this way:
In my Login Component
this.$axios.post('login')
.then((res)=>{
localstorage.setItem('access-token', res.data.access_token);
})
Now in another component when sending a post request
export default{
methods:{
getvals(){
localstorage.getItem('access-token') //do stuff after retrieve
}
}
}
Thats just one example, Imagine what could happen when setting multiple localstorage items when retrieving one can type the wrong key.
How can i centralize functionality eg: setting token(in angular2 would be services)
There are a few different ways to share functionality between components in Vue, but I believe the most commonly used are either mixins or custom modules.
Mixins
Mixins are a way to define reusable functionality that can be injected into the component utilizing the mixin. Below is a simple example from the official Vue documentation:
// define a mixin object
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
Custom module
If there are a lot of shared functionality with a logical grouping it might make sense to instead create a custom module, and import that where you need it (like how you inject a service in angular).
// localStorageHandler.js
const localStorageHandler = {
setToken (token) {
localStorage.setItem('access-token', token)
},
getToken () {
localstorage.getItem('access-token')
}
}
export default localStorageHandler
And then in your component:
// yourcomponent.vue
import localStorageHandler from 'localStorageHandler'
export default{
methods:{
getvals(){
const token = localStorageHandler.getToken()
}
}
}
Modules are using the more modern syntax of JavaScript, which is not supported in all browsers, hence require you to preprocess your code. If you are using the vue-cli webpack template it should work out of the box.