Do I have to await .vm.$emit()? - vue.js

I have a Vue 3 application and use vue-test-utils for testing.
I also use web components. One of the components is an input element that implements an inputChanged event that is triggered in case the input is changed.
Lets say the inputChanged event leads to data being rendered in the template:
Template:
...
<InputWebComponent
id="email-input"
label="Email"
:value="currentEmail"
#inputChanged="onEmailChanged"/>
...
Code:
onEmailChanged(event: CustomEvent<string>) {
this.currentEmail = event.detail;
}
When it comes to testing such an event, I found examples where people use the following in the test:
const wrapper = shallowMount(ComponentToBeTested, { props: ... });
// emitting the event
await wrapper.findComponent(InputWebComponent).vm.$emit('inputChanged', { detail: 'some#mail.com' });
// test if something happened
...
Here is my question:
The web component does not return a promise when emitting inputChanged. And the examples I found in the documentation (note that the examples have changed a little for the Vue 3 documentation and for some reason, do no longer use .vm.$emit()) there is no await added before emitting.
Also the test works without the await in my code.
Do I have to await an event emitted in the test via .vm.$emit()?

Related

How to test my tooltip component using Vue.js and Jest?

I'm trying to test my tooltip component, but it seems it does not exist :cry:
My .html
<div>
<boxComponent>
Some text
<tooltipComponent
#mouseover.native="handleHover(true)"
#mouseleave.native="handleHover(false)"
>This text appears on Hover</tooltipComponent>
</boxComponent>
<switchComponent button-type="button" :label="false" #change="activeFun" />
</div>
My .js
methods: {
handleHover (s) {
this.onHoverTooltip = s
},
}
My .spec.js
const localVue = createLocalVue()
localVue.use(Vuex)
//...
it('should reveal tooltip\'s mesage', () => {
const wrapper = shallowMount(ozFilters, {
propsData: {
//others stuffs,
label: false,
},
localVue,
store,
stubs: ['tooltipComponent', 'boxComponent', 'switchComponent'],
})
expect(wrapper.find('tooltipComponent-stub').exists()).toBeFalsy()
// wrapper.vm.label = true
wrapper.vm.handleHover(true)
expect(wrapper.find('tooltipComponent-stub').exists()).toBeTruthy()
})
I need to understand what should I do to test the tooltip component that is already a custom component.
Even without the -stub it does not work.
The error is occurring in this line expect(wrapper.find('tooltipComponent-stub').exists()).toBeTruthy() with says that the expect is false.
Well, there are a couple of things that need to be fixed/clarified.
First of all, you are using shallowMount to create a component which you want to test, it stubs all custom components provided in tested component so you don't have to additionally use stub parameter to stub them, you can easily delete this: stubs: ['tooltipComponent', 'boxComponent', 'switchComponent'].
When you want to find specific component it's recommended to use findComponent instead of just find. (Using find to search for a component is actually deprecated). To properly identify component it's best to import it in your test file and use it in findComponent argument, something like this:
import BoxComponent from 'path/to/BoxComponent'
it('lovely test', () => {
wrapper.findComponent(BoxComponent)
})
After this your test should pass, but there are some things to consider like using mount instead of shallowMount. It's explained here, but to wrap it up, mount will render actual component with its children, where shallowMount stubs components inside tested component, so you are not testing the actual component but some "shadow" of its true nature. To see a difference between this functions I would recommend to create a wrapper with both functions and then see what html() will return from them.

Vue test utils - setChecked() not updating v-model

I am writing unit tests for some components I made at my job. We are using Mocha (TDD) and the Chai assertion library. I have a component with some checkboxes, and using the setChecked() method on them from vue-test-utils is not behaving as expected. I have made a small example that reproduces the error:
TestComponent.vue:
<template>
<div>
<input class="checkboxTest" type="checkbox" v-model="cbVal">
<input class="inputTest" type="text" v-model="textVal">
</div>
</template>
<script>
define([], function() {
return {
data: function() {
return {
cbVal: false,
textVal: ""
}
}
}
})
</script>
test.js:
suite("Random test", function() {
var VueTest;
var TestComponent;
//Import the vue test utils library and TestComponent
suiteSetup(function(done) {
requirejs(
["vue-test-utils", "vuec!components/TestComponent"],
function(VT, TC) {
VueTest = VT;
TestComponent = TC;
done();
}
);
});
//This test passes
test("fill in the input", function() {
var wrapper = VueTest.mount(TestComponent);
wrapper.find(".inputTest").setValue("Hello, world!");
assert.equal(wrapper.vm.textVal, "Hello, world!");
});
//This one does not
test("programatically check the box", function() {
var wrapper = VueTest.mount(TestComponent);
wrapper.find(".checkboxTest").setChecked(true);
//Prints out AssertionError: expected false to equal true
assert.equal(wrapper.vm.cbVal, true);
});
});
The textVal data member in TestComponent is getting changed, but cbVal is not. Can anyone please explain why setValue() works just fine, but setChecked() does not? Thank you in advance.
I had a similar issue and the accepted answer did not solve my problem. I don't think the accepted answer is correct either, as setChecked was added specifically to avoid having to manually set the values via the elements.
In my case, I wanted Vue to react to the v-model change and redraw. I tried async and many other methods, until finding the one that works: wrapper.vm.$forceUpdate().
Here's what my working code looks like:
wrapper.find("#someRadioButtonId").setChecked(true)
// manually force Vue to update
wrapper.vm.$forceUpdate()
expect(wrapper.find("#someRadioButtonId").classes()).toContain("selected") // success!
I can't answer why it doesn't work, but I can tell you your approach is incorrect in the first place.
You shouldn't be interacting with the html elements directly to set their values. When you set vue-model to cbVal you should instead be interacting with cbVal.
In other words, change your code from setChecked() to cbVal = true in order for it to comply with how Vue wants you to develop your project. There's no guarantee Vue can remain dynamic and reactive if you don't interact with your code the way Vue wants you to.

vue.js: scoping custom option merge strategies instead going global

Good Day Fellows,
Quick summary: how can I use custom option merge strategies on an individual basis per component and not globaly?
My problem:
I am extending my components via Mixins and it is working great so far. However, while it is working great with the likes of component methods, I often need to override some lifecycle hooks, like mounted, created, etc. The catch is, Vue - by default - queues them up in an array and calls them after another. This is of course defined by Vues default merge strategies.
However in some specific cases I do need to override the hook and not have it stack. I know I can customize Vue.config.optionMergeStrategies to my liking, but I want the mergeStrategy customized on a per component basis and not applying it globably.
My naive approach on paper was to create a higher function which stores the original hooks, applies my custom strategy, calls my component body and after that restores Vues original hooks.
Let's say like this
export default function executeWithCustomMerge(fn) {
const orig = deep copy Vue.config.optionMergeStrategies;
Vue.config.optionMergeStrategies.mounted = (parent, child) => [child];
fn();
Vue.config.optionMergeStrategies = deep copy orig;
}
And here's it in action
executeWithCustomMerge(() => {
Vue.component('new-comp', {
mixins: [Vue.component("old-comp")],
},
mounted() {
//i want to override my parent thus I am using a custom merge strategy
});
});
Now, this is not going to work out because restoring the original hook strategies still apply on a global and will be reseted before most hooks on my component are being called.
I wonder what do I need to do to scope my merge strategy to a component.
I had a look at optionMergeStrategies in more detail and found this interesting quote from the docs (emphasis mine):
The merge strategy receives the value of that option defined on the parent and child instances as the first and second arguments, respectively. The context Vue instance is passed as the third argument.
So I thought it would be straightforward to implement a custom merging strategy that inspects the Vue instance and looks at its properties to decide which strategy to use. Something like this:
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal, vm) {
if (vm.overrideCreated) {
// If the "overrideCreated" prop is set on the component, discard the mixin's created()
return [vm.created];
}
return mergeCreatedStrategy(toVal, fromVal, vm);
};
It turns out though that the 3rd argument (vm) is not set when the strategy function is called for components. It's a new bug! See https://github.com/vuejs/vue/issues/9623
So I found another way to inform the merge strategy on what it should do. Since JavaScript functions are first-class objects, they can have properties and methods just like any other object. Therefore, we can set a component's function to override its parents by setting a property on it and looking for its value in the merge strategy like so:
Vue.mixin({
created() {
this.messages.push('global mixin hook called');
}
});
const mixin = {
created() {
this.messages.push('mixin hook called');
},
};
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal) {
if (fromVal.overrideOthers) {
// Selectively override hooks injected from mixins
return [fromVal];
}
return mergeCreatedStrategy(toVal, fromVal);
};
const app = {
el: '#app',
mixins: [mixin],
data: { messages: [] },
created() {
this.messages.push('component hook called');
},
};
// Comment or change this line to control whether the mixin created hook is applied
app.created.overrideOthers = true;
new Vue(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Messages from hooks</h1>
<p v-for="message in messages">{{ message }}</p>
</div>

VueJS - Update Parent Data After Ajax

I'm trying out VueJS, with the aim of incrementally updating a jQuery project, but I'm having issues with props in a child component passed through data from a parent component.
My parent component is
// parent_component.js
var parentData = {};
var parentComponent = Vue.component('parentComponent', {
data: function() {
return {
parentData: _parentData
}
},
delimiters: ['((', '))']
})
$.ajax({
url: '/someUrl/',
success: function(response) {
_parentData = response
}
My Child component is:
// child_component.js
Vue.component('child-component', {
template: '<div>((data.someProp))</div>'
props: ['data'],
delimiters: ['((', '))']
})
My HTML is:
// index.html
<parent-component inline-template>
<child-component v-bind:data="parentData"></child-component>
</parent-component>
This all works fine when I update _parentData right after initializing the childComponent. But I actually need to do an Ajax call and update the _parentData, it is not updated in the childComponent.
Nb. I checked that the _parentData object is there in the callback of the Ajax call.
Also, I tried putting the Ajax call in the created option of the childComponent, but this didn't help.
What am I missing?
Update
I guess I made the classic beginner's mistake! As stated here, Vue cannot detect property addition. So I need to define the someProp property on the _parentData before making the async call.
So if I define var parentData = { someProp: '' }; it will work.
See this fiddle: https://jsfiddle.net/maaikeb/anr1e88n/
If you share the AJAX part of code, maybe I can help better, but IMHO I think that the easiest way is emiting a event from children, and catch it with the parent element when the AJAX call is done (success or error).
Events provide a way to inform your parent components of changes in children.
Template Usage:
<my-component v-on:myEvent="parentHandler"></my-component>
<!-- Or more succinctly, -->
<my-component #myEvent="parentHandler"></my-component>
Firing an Event:
...
export default {
methods: {
fireEvent() {
this.$emit('myEvent', eventValueOne, eventValueTwo);
}
}
}
Additionally, you can create global event buses to pass events anywhere in your app
Extracted from here.
Edit
Ok, I didn't understand you well, if you have problems sending data down from parent to children when updating the parent the proper way is the double data binding.
Maybe your problem is that the data is only evaluated in component creation, and your child will be created with the initial value of the parent component...
You can try some different things to solve this, like:
1 - Maybe your issue is related to a change detection caveat
Maybe you're creating a new property in an object...
_parentData = response
...when you should do...
_parentData.$set('someObject.someProperty', someValue)
2 - Use watch on parent instead of created:
watch:{
yourData() {
$.ajax({
url: '/someUrl/',
content_type: 'application/json',
data_type: 'json',
success: function(response) {
_parentData = response
}
})
},
3 - Try using .sync ( deprecated in 2.0 ):
<component :something.sync="variable"></component>
4 - Bind .this to the anonymous AJAX call or use arrow functions as #Bert commented:
success: () => (response) {
_parentData = response
}
Hope it helps!

Vue 2 hook ready

Vue 2, Is there a lifecycle hook that actually refers to "finished rendering"? I want to put a loading screen when entering a page and it fades away and show the page content once Vue is finished loading everything, but I have tried most of the lifecycle hook but not working. Here is something I would try to do if updated refers to "finished rendering":
updated(){
this.loaded()
},
methods:{
loaded(){
var vm = this;
this.loading = false;
}
}
If there is not such a lifecycle hook, what would you suggest me to do instead? Thanks
Probably the method you're looking for is mounted(), this is fired when the vue component is ready. You can check the Vue life-cycle documentation here
so probably your Vue instance would look something like this:
var app = new Vue({
el: '#app',
/*
* When the instance is loaded up
*/
mounted: function () {
this.loaded()
},
methods: {
loaded: function () {
var vm = this
this.loading = false
}
}
})
To make sure that all child components have also been mounted, use vm.$nextTick - much cleaner than a setTimeout:
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
Source: https://v2.vuejs.org/v2/api/#Options-Lifecycle-Hooks
As I cannot find that there is a specific lifecycle hook or other Vue-specified method to hook the point where everything is finished rendering, I used the basic js code as well as estimation of time needed for loading all the things with codes as following:
mounted(){
this.loaded()
},
methods:{
loaded(){
var vm = this;
setTimeout(function(){
vm.loading = false
}, 3000);
}
}
Hope there will be someone who is strong at Vue and provide a new answer to hook it more accurately.
Actually, your answer to your question is not what you have asked originally, the Vue instance is rendered in less than a second and it's not possible to use any hooks only to create userfriendly loader.
The case in using setTimeout function is the only solution for this. But the component is ready in DOM a way earlier than 3 seconds.
The best approach for this purpose is to use loader component, where you will have setTimeout in created hook and after this time it will emit event to the parent.
So inside app you need only to import the loader component itself and when the event fires, change internal boolean data property to render another component v-if