How can I test a custom input Vue component - vue.js

In the Vue.js documentation, there is an example of a custom input component. I'm trying to figure out how I can write a unit test for a component like that. Usage of the component would look like this
<currency-input v-model="price"></currency-input>
The full implementation can be found at https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
The documentation says
So for a component to work with v-model, it should (these can be configured in 2.2.0+):
accept a value prop
emit an input event with the new value
How do I write a unit test that ensures that I've written this component such that it will work with v-model? Ideally, I don't want to specifically test for those two conditions, I want to test the behavior that when the value changes within the component, it also changes in the model.

You can do it:
Using Vue Test Utils, and
Mounting a parent element that uses <currency-input>
Fake an input event to the inner text field of <currency-input> with a value that it transforms (13.467 is transformed by <currency-input> to 13.46)
Verify if, in the parent, the price property (bound to v-model) has changed.
Example code (using Mocha):
import { mount } from '#vue/test-utils'
import CurrencyInput from '#/components/CurrencyInput.vue'
describe('CurrencyInput.vue', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data: { price: null },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
})
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
In-browser runnable demo using Jasmine:
var CurrencyInput = Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)">\
</span>\
',
props: ['value'],
methods: {
// Instead of updating the value directly, this
// method is used to format and place constraints
// on the input's value
updateValue: function(value) {
var formattedValue = value
// Remove whitespace on either side
.trim()
// Shorten to 2 decimal places
.slice(0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3)
// If the value was not already normalized,
// manually override it to conform
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// Emit the number value through the input event
this.$emit('input', Number(formattedValue))
}
}
});
// specs code ///////////////////////////////////////////////////////////
var mount = vueTestUtils.mount;
describe('CurrencyInput', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data() { return { price: null } },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
});
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
// load jasmine htmlReporter
(function() {
var env = jasmine.getEnv()
env.addReporter(new jasmine.HtmlReporter())
env.execute()
}())
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css">
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script src="https://npmcdn.com/vue#2.5.15/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-template-compiler#2.5.15/browser.js"></script>
<script src="https://rawgit.com/vuejs/vue-test-utils/2b078c68293a41d68a0a98393f497d0b0031f41a/dist/vue-test-utils.iife.js"></script>
Note: The code above works fine (as you can see), but there can be improvements to tests involving v-model soon. Follow this issue for up-to-date info.

I would also mount a parent element that uses the component. Below a newer example with Jest and Vue Test Utils. Check the Vue documentation for more information.
import { mount } from "#vue/test-utils";
import Input from "Input.vue";
describe('Input.vue', () => {
test('changing the input element value updates the v-model', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Brendan Eich';
await wrapper.find('input').setValue(name);
expect(wrapper.vm.$data.name).toBe(name);
});
test('changing the v-model updates the input element value', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Bjarne Stroustrup';
await wrapper.setData({ name });
const inputElement = wrapper.find('input').element;
expect(inputElement.value).toBe(name);
});
});
Input.vue component:
<template>
<input :value="$attrs.value" #input="$emit('input', $event.target.value)" />
</template>

Related

How can I access modelValue from inside a direct in Vue 3

The question is: How to access el.modelValue and update it from custom directive?
directives: {
currency: {
mounted(el, binding, vnode, prevVnode) {
console.log(vnode, prevVnode)
el.oninput = (e) => {
if (Number.isNaN(e.data)) return
el.value = e.data
e.oninput = () => {
binding.value += 1
}
}
},
},
For reading, if you want to be sure that the value of the v-model is used instead of the value of the input (as they can differ) you could set the variable that is used for the v-model as directive value. As of mutating, you should dispatch the appropriate event to the element and let Vue handle the update of the v-model. Like in this example below:
StackBlitz
<template>
<input v-focus="value" v-model="value" type="text" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('some string');
function patchInputValue(el, value) {
var event = new Event('input', { bubbles: true });
el.value = value;
el.dispatchEvent(event);
}
const vFocus = {
mounted: (el, binding) => {
// binding.value -> 'some string'
patchInputValue(el, 'new value');
},
};
</script>
Note that new Event() won't work in IE <11. There are a couple of polyfill packages that provide compatibility.

Vue 3 Composition API - Props default and DOM inside lifecycle methods

I have a Vue component inside a NuxtJS app and I'm using the #nuxtjs/composition-api.
I have this component which is a <Link> component and I would like to make the code clearer.
I have a computed property that determines to color of my UiIcon from iconColor, iconColorHover, IconActive. But most importantly, I want to set it to a specific color if I have a disable class on my root component. It works like that but it doesn't look too good I believe.
I found out that undefined is the only value that I can use to take UiIcon default props if not defined. Empty string like '' would make more sense to more but it's considered as a valid value. I would have to do some ternary conditions in my UiIcon and I'd like to avoid that.
<template>
<div ref="rootRef" class="row">
<UiIcon
v-if="linkIcon"
:type="linkIcon"
:color="linkIconColor"
class="icon"
/>
<a
class="link"
:href="linkHref"
:target="linkTarget"
:rel="linkTarget === 'blank' ? 'noopener noreferrer' : null"
#mouseover="linkActive = true"
#mouseout="linkActive = false"
>
<slot></slot>
</a>
</div>
</template>
<script lang="ts">
import {
defineComponent,
computed,
ref,
toRefs,
nextTick,
onBeforeMount,
} from '#nuxtjs/composition-api';
import { Colors } from '~/helpers/styles';
export default defineComponent({
name: 'Link',
props: {
href: {
type: String,
default: undefined,
},
target: {
type: String as () => '_blank' | '_self' | '_parent' | '_top',
default: '_self',
},
icon: {
type: String,
default: undefined,
},
iconColor: {
type: String,
default: undefined,
},
iconHoverColor: {
type: String,
default: undefined,
},
},
setup(props) {
const { href, target, icon, iconColor, iconHoverColor } = toRefs(props);
const linkActive = ref(false);
const rootRef = ref<HTMLDivElement | null>(null);
const writableIconColor = ref('');
const linkIconColor = computed({
get: () => {
const linkDisabled = rootRef.value?.classList.contains('disabled');
if (linkDisabled) {
return Colors.DARK_GREY;
}
if (linkActive.value && iconHoverColor.value) {
return iconHoverColor.value;
}
return iconColor.value;
},
set: (value) => {
writableIconColor.value = value;
},
});
onBeforeMount(() => {
nextTick(() => {
const linkDisabled = rootRef.value?.classList.contains('disabled');
if (linkDisabled) {
linkIconColor.value = Colors.DARK_GREY;
}
});
});
return {
rootRef,
linkHref: href,
linkTarget: target,
linkIcon: icon,
linkIconColor,
linkActive,
};
},
});
</script>
Implementing disabled status for a component means it will handle two factors: style (disabled color) and function. Displaying a disabled color is only a matter of style/css. implementing it in programmatical way means it'll take longer time to render completely on user's side and it'll lose more SEO scores. examine UiIcon's DOM from browser and override styles using Deep selectors.
If I were handling this case, I would have described the color with css and try to minimize programmatic manipulation of style.
<template>
<div :disabled="disabled">
</div>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
}
}
}
</script>
// it does not have to be scss.
// just use anything that's
// easier to handle variables.
<style lang="scss">
// I would normally import css with prepend option from webpack,
// but this is just to illustrate the usage.
#import 'custom-styles.scss';
&::v-deep button[disabled] {
color: $disabled-color;
}
</style>
attach validator function on the props object. it'll automatically throw errors on exceptions.
{
props: {
icon: {
type: String,
default: "default-icon",
validator(val) {
return val !== "";
// or something like,
// return val.includes(['iconA', 'iconB'])
},
},
}
}

vuejs treeselect - delay loading does not work via vuex action

Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.

'this' context in functional component's child event handler

I am trying to create custom event handlers for child components/elements of the functional component. The problem is that when using a render() function to create the child components, I cannot access their this context.
Suppose we have the following functional component:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
this.$emit('input', data)
}
for (const prop of Object.keys(context.props.value)){
const child = createElement('input', {
props: {
value: context.props[prop]
},
on: {
input: function(event){
// 'this' is not binded here - it is undefined,
// hence the inputHandler() function is
// rising an error
inputHandler.apply(this, [event.target.value, prop])
}
}
})
template.push(child)
}
return template
}
}
Is it possible to access this context for a vnode, when creating event handler this way?
P.S. Use case info: I want to implement a component that automatically generates <input> elements for a resource and uses two-way binding through v-model directive. I also want to use it in <table> where wrapping in <td> will be required, thus I made the component functional.
Functional components don't a have a "this", because there is no Vue instance for them. This makes them lightweight.
This also means emiting events from them is kind of harder, since you need to implement Vue's logic yourself.
Lacking an instance doesn't mean you cannot events, instead, you need to manually parse context.listeners and call the event handler manually. In the case of v-model, you need to call the input listener:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop, handler){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
// Call handler directly instead of using this.$emit
handler(data)
}
for (const prop of Object.keys(context.props.value)){
console.log(context.props.value, prop)
const child = createElement('input', {
// Small bug fixes in the following section:
domProps: {
value: context.props.value[prop]
},
// End bug fixes
on: {
input: function(event){
// pass `context.listeners.input` instead of binding here
inputHandler(event.target.value, prop, context.listeners.input)
}
}
})
template.push(child)
}
return template
}
}
new Vue({
el: "#app",
components: {
Aggregate
},
data: {
test: {
key1: "val1",
key2: "val2",
}
},
})
<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<aggregate v-model="test"></aggregate>
<pre>{{ test }}</pre>
<button #click="test = {...test, ping: 'pong'}">Add key</button>
</div>

How to address the data of a component from within that component?

In a standalone Vue.js script I can mix functions and Vue data:
var vm = new Vue ({
(...)
data: {
number: 0
}
(...)
})
function return100 () {
return 100
}
vm.number = return100()
I therefore have a Vue instance (vm) which data is directly addressable via vm.<a data variable>)
How does such an addressing works in a component, since no instance of Vue is explicitly instantiated?
// the component file
<template>
(...)
</template>
<script>
function return100 () {
return 100
}
export default {
data: function () {
return {
number: 0
}
}
}
// here I would like to set number in data to what return100()
// will return
??? = return100()
</script>
You can achieve the target by using code like this.
<template>
<div>{{ name }}</div>
</template>
<script>
const vm = {
data() {
return {
name: 'hello'
};
}
};
// here you can modify the vm object
(function() {
vm.data = function() {
return {
name: 'world'
};
}
})();
export { vm as default };
</script>
But I really don't suggest you to modify data in this way and I think it could be considered as an anti-pattern in Vuejs.
In almost all the use cases I met, things could be done by using Vue's lifecycle.
For example, I prefer to write code with the style showed below.
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
data() {
return {
name: 'hello'
};
},
mounted() {
// name will be changed when this instance mounted into HTML element
const vm = this;
(function() {
vm.name = 'world';
})();
}
};
</script>