How can I validate a custom select component made of a button using VeeValidate? - vue.js

I'm trying to build a custom select input, the functionality works as expected but not with the validation. I can't get it work by following the example since a <button> can't have a v-model. I don't think I can use a computed get-set approach like shown in the docs because I only use computed prop to get the text of the selected value to be shown in the UI, not to be sent to parent, nor to set a value.
Setting the value are done using #click="$emit(...)" on <li> element.
To be clear, here I provide both the script and the implementation, please have a look
BaseSelect.vue
<template>
<ValidationProvider
tag="div"
:vid="name"
:mode="validationMode"
:rules="validationRules"
v-slot="{ errors }"
>
<label :id="name" class="block font-medium mx-2 mb-3">{{ label }}</label>
<div class="relative">
<button
type="button"
aria-haspopup="listbox"
:aria-expanded="open"
:aria-labelledby="name"
class="relative w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
#click.prevent="open = !open"
>
<span v-if="value">{{ selected }}</span>
<span v-else>{{ placeholder }}</span>
<!-- selectable indicator icon (svg) -->
...
</button>
<!-- Error message -->
<small v-if="errors.length > 0" class="text-red-500 mt-3 mx-2">{{
errors[0]
}}</small>
<transition
leave-active-class="transition ease-in duration-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<div
class="absolute mt-1 w-full rounded-md bg-white dark:bg-gray-800 shadow-lg"
v-show="open"
>
<ul
tabindex="-1"
role="listbox"
:aria-labelledby="name"
aria-activedescendant="listbox-item-3"
class="max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<li
v-for="item in items"
:key="item.id"
:id="`listbox-item-${item.id}`"
role="option"
class="text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default select-none relative py-2 pl-3 pr-9"
#click="$emit('input', item[preferedValue])"
>
<span
class="ml-3 block font-normal truncate"
v-text="item.name"
/>
<!-- Checkmark, only display for selected option. (svg) -->
...
</li>
</ul>
</div>
</transition>
</div>
</ValidationProvider>
</template>
<script>
import { ValidationProvider } from 'vee-validate'
export default {
inheritAttrs: false,
components: { ValidationProvider },
props: {
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
hideLabel: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: 'Choose one',
},
value: {
type: [String, Number, Object],
default: null,
},
required: {
type: Boolean,
default: true,
},
rules: {
type: String,
required: false,
},
validationMode: {
type: String,
default: 'eager',
},
items: {
type: Array,
required: true,
default: () => [],
},
preferedValue: {
type: String,
default: 'id',
},
},
data: () => ({
open: false,
}),
computed: {
/**
* The text to be shown based on the selected item and
* the prefered value as it's key.
*
* #returns string
*/
selected() {
if (this.value === null || !this.items.length) return ''
let index = _.findIndex(
this.items,
(item) => item[this.preferedValue] === this.value
)
return this.items[index].name
},
/**
* The validation rules to be applied in this input field.
*
* #returns string
*/
validationRules() {
if (!this.required) return this.rules
return this.rules ? `required|${this.rules}` : 'required'
},
},
mounted() {
document.addEventListener('click', this.close)
},
destroyed() {
document.removeEventListener('click', this.close)
},
methods: {
toggle() {
this.open = !this.open
},
close(e) {
if (!this.$el.contains(e.target)) {
this.open = false
}
},
},
}
</script>
implementation.vue
<template>
<BaseForm
ref="form"
reference="subcategory_form"
:errors="errors"
action="/subcategories"
method="POST"
#submit="store"
>
...
<BaseSelect
class="mb-4"
name="category_id"
label="Category"
placeholder="Choose one"
:items="categories.data"
v-model="subcategory.category_id"
/>
</BaseForm>
</template>
<script>
layout: 'dashboard',
async fetch() {
await this.$store.dispatch('categories/load')
},
data: () => ({
subcategory: {
category_id: null,
name: '',
},
errors: {},
}),
computed: {
categories() {
return this.$store.state.categories.pagination
},
},
methods: {
async store() {
...
},
},
}
</script>
BaseForm.vue
<template>
<ValidationObserver :ref="reference" tag="div" v-slot="{ handleSubmit }">
<form
:action="action"
:method="method"
#submit.prevent="handleSubmit(onSubmit)"
>
<slot></slot>
</form>
</ValidationObserver>
</template>
<script>
import { ValidationObserver } from 'vee-validate'
export default {
components: { ValidationObserver },
props: {
action: {
type: String,
required: true,
},
method: {
type: String,
required: true,
},
reference: {
type: String,
required: true,
},
errors: {
type: Object,
default: () => {},
},
},
watch: {
/**
* Watch for `errors`.
*
* Everytime it changes, assuming it comes from the backend,
* assign the errors to the `ValidationObserver`.
*/
errors(val) {
this.$refs[this.reference].setErrors(val)
},
},
methods: {
/**
* Emit `submit` event to the parent component.
*
* #returns void
*/
onSubmit() {
this.$emit('submit')
},
},
}
</script>
So, is this possible to validate with vee?

You can use the validate function on the slot props of the ValidationProvider, there is a guide for this here.
https://vee-validate.logaretm.com/v3/advanced/model-less-validation.html#html-file-validation
The validate function accepts the input value to be set as the current value internally which will be validated.
<ValidationProvider v-slot="{ validate }">
<button #click="validate(someValue)"></button>
</ValidationProvider>

Related

How to make a validation and show message if default value is selected in dropdown in vue

I have a dropdown that I want to give a validation.
So here is my dropdown component:
<template>
<div class="custom-select" :tabindex="tabindex" #blur="open = false">
<div class="selected" :class="{ open: open }" #click="open = !open">
{{ selected.name }}
</div>
<div class="items" :class="{ selectHide: !open }">
<div v-if="defaultValue != ''">{{defaultValue}}</div>
<div v-for="(option, i) of options" :key="i" #click=" selected = option; open = false; $emit('input', option);">
{{ option.name }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
options: {
type: Array,
required: true,
},
defaultValue: {
type: String,
required: false,
default: "Choose an option",
},
tabindex: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
open: false,
selected: this.setDefaultValue(),
};
},
mounted() {
this.$emit("input", this.selected);
},
methods: {
setDefaultValue () {
if (this.defaultValue == '' && this.options.length > 0) {
return this.options[0];
}
return { name: this.defaultValue};
}
}
};
</script>
and in my parent component, I am using this dropdown, the fetched value from API call and filled with variations. So what I am trying to do is if the value is not selected (default: "Choose an option"), I want to give an error message which is saying, the dropdown is mandatory.
<Dropdown
:options="getVariations"
class="select"
#input="getSelected"
/>
<script>
import Dropdown from "../components/Dropdown";
import apiHelper from "../helpers/apiHelper";
export default {
components: {Dropdown},
data() {
return {
selected: "",
variationId: "",
selectedObject: null
};
},
computed: {
getVariations() {
return this.product.attributes.variations
}
},
methods: {
getSelected(opt) {
this.selectedObject = opt;
this.selected = opt.description;
this.variationId = opt.id;
}
},
};
</script>
This is a simple example of what you are asking for:
// v-model property
selectedVModel: null
// html
<select id="testselect" v-model="selectedVModel" required>
<option :value="null">Default</option>
<option value="bar">Option 1</option>
<option value="foo">Option 2</option>
<option value="baz">Option 3</option>
</select>
<div v-if="!selectedVModel">Error! Choose something!</div>
You can handle the error inside of your child component. In this example, if the v-model as selectedVModel is empty, the <div> with v-if="!selectedVModel" will be shown. As selectedVModel is null it will automatically select <option :value="null">Default</option>. If you chosse one of the options selectedVModel get´s a value so v-if="!selectedVModel" results into false, no error then. If you select Default again, the value turns to null again which results into true at v-if="!selectedVModel", so the error will be visible again.
Combine this simple example with your code and let me know if it helped you.
If you want to validate in your parent component, try to check selectedObject.id :
Vue.component('Dropdown', {
template: `
<div class="custom-select" :tabindex="tabindex" #blur="open = false">
<div class="selected" :class="{ open: open }" #click="open = !open">
{{ selected.name }}
</div>
<div class="items" :class="{ selectHide: !open }">
<div v-if="defaultValue != ''">{{defaultValue}}</div>
<div v-for="(option, i) of options" :key="i" #click=" selected = option; open = false; $emit('input', option);">
{{ option.name }}
</div>
</div>
</div>
`,
props: {
options: {
type: Array,
required: true,
},
defaultValue: {
type: String,
required: false,
default: "Choose an option",
},
tabindex: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
open: false,
selected: this.setDefaultValue(),
};
},
mounted() {
this.$emit("input", this.selected);
},
methods: {
setDefaultValue () {
if (this.defaultValue == '' && this.options.length > 0) {
return this.options[0];
}
return { name: this.defaultValue};
}
}
})
new Vue({
el: '#demo',
data() {
return {
selected: "",
variationId: "",
selectedObject: null,
product: {
attributes: {
variations: [{id: 1, name: 'name1', description: 'desc1'}, {id: 2, name: 'name2', description: 'desc2'},{id: 3, name: 'name3', description: 'desc3'},]
}
}
};
},
computed: {
getVariations() {
return this.product.attributes.variations
}
},
methods: {
getSelected(opt) {
this.selectedObject = opt;
this.selected = opt.description;
this.variationId = opt.id;
},
submit() {
if (!this.selectedObject.id) alert('pls select option')
else console.log(this.selectedObject)
}
},
})
Vue.config.productionTip = false
Vue.config.devtools = false
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<button #click="submit">submit</button>
<Dropdown
:options="getVariations"
class="select"
#input="getSelected"
/>
</div>

Vuex / Filter: preserve input value on navigation

Does anyone know how can I select an item from my filteredResults array and display it as the input's value? Furthermore, I need that on page navigation this input value to remain the same if I refresh or navigate back and forth through the pages.
<template>
<div class="search-input relative">
<label class="block">
<span class="text-gray-700 font-bold">{{ label }}</span>
<input
type="search"
class="mt-1 p-2 block min-w-full border-0 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 bg-transparent border-b-2 border-blue-500"
autocomplete="off"
:placeholder="placeholder"
v-model.trim="keyword"
#click="resetAirport"
>
</label>
<ul v-if="filteredResults != null" class="mt-2 absolute shadow-md rounded-lg">
<li v-for="result in filteredResults" :key="result.id" class="p-2 bg-white hover:bg-gray-100" #click="selectAirport(result)">
<NuxtLink to="/" class="block">
{{ result.icao }}, {{ result.iata }} {{ result.name }}
</NuxtLink>
</li>
</ul>
<NuxtLink :to="`/airport/LRAR`" class="text-gray-300 mt-2 text-xs text-right">Details</NuxtLink>
</div>
</template>
<script>
import { defineComponent } from '#vue/composition-api'
import { mapGetters, mapActions } from 'vuex'
export default defineComponent({
setup(props) {},
data() {
return {
keyword: '',
}
},
computed: {
...mapGetters({
getAirports: 'getAirports'
}),
filteredResults() {
if (this.keyword.length < 3) return null
return this.getAirports.filter(item => {
if (!item.icao) return false
return item.icao
.toString()
.toLowerCase()
.includes(this.keyword.toLowerCase())
})
},
},
methods: {
selectAirport: function(airport) {
this.addAirport({airport: airport, type: this.type})
this.keyword = `${airport.icao}, ${airport.iata} (${airport.city}) / ${airport.name}`
},
resetAirport: function() {
if (this.keyword != null) {
this.removeAirport(this.type)
this.keyword = ''
}
},
...mapActions([
'addAirport',
'removeAirport'
])
},
props: {
label: String,
placeholder: String,
type: String,
}
})
</script>
Here is my store:
export const state = () => ({
airports: [],
pairing: {
departure: null,
arrival: null
},
loading: false
})
export const mutations = {
SET_AIRPORTS(state, payload) {
state.airports = payload
},
SET_AIRPORT(state, { airport, type }) {
state.pairing[type] = airport
},
CLEAR_AIRPORT(state, type) {
state.pairing[type] = null
}
}
export const actions = {
addAirport({ commit }, { airport, type }) {
commit('SET_AIRPORT', { airport, type })
},
removeAirport({ commit }, type) {
commit('CLEAR_AIRPORT', type)
}
}

radio checked between 2 values

I am modifying a module on Prestashop (Blockwishlist) which uses VueJS and I want to add the available combinations in the customers' wish list in order to be able to select a combination and add it to the cart.
But I'm stuck on a part when loading the page: I get the id_product and the id_product_attribute and I also loop on all the variations of the product.
I am looking for a checked radio button by comparing the values ​​of my loop and the id_product_attribute value that I get back and if it exists in my loop this checked the radio button.
This is what my vuejs code looks like now (with attempts that don't work)
<template>
<div class="wishlist-product">
<a
class="wishlist-product-link"
:href="product.canonical_url"
>
<div class="wishlist-product-image">
<img
v-if="product.default_image"
:src="product.default_image.large.url"
:alt="product.default_image.legend"
:title="product.default_image.legend"
:class="{
'wishlist-product-unavailable': !product.add_to_cart_url
}"
>
<img
v-else
:src="prestashop.urls.no_picture_image.bySize.home_default.url"
>
</div>
<div class="wishlist-product-right">
<p class="wishlist-product-title">{{ product.name }}</p>
<p class="wishlist-product-price">
<span
class="wishlist-product-price-promo"
v-if="product.has_discount"
>
{{ product.regular_price }}
</span>
{{ product.price }}
</p>
<div class="wishlist-product-combinations">
<div class="product-variants">
<div class="clearfix product-variants-item">
<div id="group_1" class="radio">
<div v-for="(attribute, key, index) of product.all_attributes" class="input-container float-xs-left"
:class="{'no_stock_variant' : attribute.quantity <= 0}">
<label>
<input
#change="changeAttribute($event)"
v-model="id_product_attribute"
name="id_product_attribute"
class="input-radio" type="radio"
:data-id-product=attribute.id_product
:data-id-product-attribute=attribute.id_product_attribute
:data-product-attribute=attribute.id_attribute
:value="key"
v-if="checkedId(attribute.id_product_attribute) ? 'checked=checked' : ''"
:title=attribute.attribute_name>
<div class="content">
<span class="radio-label">
{{ attribute.attribute_name }}
<img v-if="attribute.quantity <= 0"
:src="imgEnveloppe"
:class="{hover: isHovering}"
#mouseover="imgEnveloppe = imgRing"
#mouseout="isHovering = false"
:alt="attribute.attribute_name"
>
</span>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</a>
<div class="wishlist-product-bottom">
<button
class="btn btn-indies-gold wishlist-button-add"
v-if="!isShare"
#click="removeFromWishlist"
>
{{ deleteText }}
</button>
<button
class="btn btn-indies wishlist-product-addtocart"
:disabled="isDisabled"
#click="
product.add_to_cart_url || product.customizable === '1'
? addToCartAction()
: null
"
>
{{ product.customizable === '1' ? customizeText : addToCart }}
</button>
</div>
<p
class="wishlist-product-availability wishlist-product-availability-responsive"
v-if="product.show_availability"
>
<i
class="material-icons"
v-if="product.availability === 'unavailable'"
>
block
</i>
<i
class="material-icons"
v-if="product.availability === 'last_remaining_items'"
>
warning
</i>
{{ product.availability_message }}
</p>
</div>
</template>
<script>
import EventBus from '#components/EventBus';
import headers from '#constants/headers';
import prestashop from 'prestashop';
import wishlistAddProductToCartUrl from 'wishlistAddProductToCartUrl';
export default {
name: 'Product',
props: {
product: {
type: Object,
required: true,
default: null,
},
deleteText: {
type: String,
required: true,
},
imgEnveloppe: {
type: String,
required: true,
},
imgRing: {
type: String,
required: true,
},
listId: {
type: Number,
required: true,
default: null,
},
isShare: {
type: Boolean,
required: false,
default: false,
},
customizeText: {
type: String,
required: true,
default: 'Customize',
},
quantityText: {
type: String,
required: true,
default: 'Quantity',
},
addToCart: {
type: String,
required: true,
},
status: {
type: Number,
required: false,
default: 0,
},
hasControls: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
prestashop,
isHovering: false,
id_product_attribute: null
};
},
computed: {
isDisabled() {
if (this.product.customizable === '1') {
return false;
}
return !this.product.add_to_cart_url;
},
},
methods: {
checkedId (idProductAttribute) {
if (this.product.id_product_attribute === parseInt(idProductAttribute)) {
console.log(idProductAttribute);
return true;
}
return false;
},
changeAttribute(e) {
this.id_product_attribute = e.target.getAttribute('data-id-product-attribute');
},
/**
* Remove the product from the wishlist
*/
async removeFromWishlist() {
EventBus.$emit('showDeleteWishlist', {
detail: {
listId: this.listId,
productId: this.product.id,
productAttributeId: this.product.id_product_attribute,
},
});
},
async addToCartAction() {
if (this.product.add_to_cart_url && this.product.customizable !== '1') {
try {
const datas = new FormData();
datas.append('qty', this.product.wishlist_quantity);
datas.append('id_product', this.product.id_product);
datas.append('id_product_attribute', this.id_product_attribute);
datas.append('id_customization', this.product.id_customization);
const response = await fetch(
`${this.product.add_to_cart_url}&action=update`,
{
method: 'POST',
headers: headers.addToCart,
body: datas,
},
);
const resp = await response.json();
prestashop.emit('updateCart', {
reason: {
idProduct: this.product.id_product,
idProductAttribute: this.id_product_attribute,
idCustomization: this.product.id_customization,
linkAction: 'add-to-cart',
},
resp,
});
/* eslint-disable */
const statResponse = await fetch(
`${wishlistAddProductToCartUrl}&params[idWishlist]=${this.listId}&params[id_product]=${this.product.id_product}&params[id_product_attribute]=${this.id_product_attribute}&params[quantity]=${this.product.wishlist_quantity}`,
{
headers: {
'Content-Type':
'application/x-www-form-urlencoded; charset=UTF-8',
Accept: 'application/json, text/javascript, */*; q=0.01'
}
}
);
/* eslint-enable */
await statResponse.json();
} catch (error) {
prestashop.emit('handleError', {
eventType: 'addProductToCart',
resp: error,
});
}
} else {
window.location.href = this.product.canonical_url;
}
},
},
};
Thank you for help.

Testing a wrapped vuetify autocomplete component v menu does not open in html

I want to test my custom component via vue-test-utils. I use vuetify autocomplete component in this case. I just wrapped with div and extract as a custom component. Nothing to do special so far.
This component is really works on development. I have no problem with it.
I used this component as shown below.
<my-component
id="brand-dropdown"
ref="brand-dropdown"
:items="brands"
labelKey="product.brand"
item-text="name"
item-value="name"
v-model="product.brandName"/>
My custom component looks like:
<template>
<div class="comboContainer">
<v-autocomplete
class="product-combobox"
ref="complete"
:items="localItems"
append-icon="$dropdownArrow"
outlined
dense
hide-details
color="#999999"
:item-text="itemText"
:item-value="itemValue"
:filter="autoCompleteFilter"
v-model="selected"
#change="onListItemSelected">
<template slot="label">
<span>{{$t(labelKey)}}</span>
</template>
<template slot="prepend-inner" v-if="showSearchIcon">
<div class="d-flex align-center justify-center"
style="height: 25px;">
<img src="~/assets/icons/search/search.png"/>
</div>
</template>
<template v-slot:no-data>
<v-list-item id="noMatchText">
<v-list-item-title>
<div :no-data-item="dataTestId">
{{ $t('selection.noMatchFound') }}
</div>
</v-list-item-title>
</v-list-item>
</template>
<template v-slot:item="{ item }">
<v-tooltip top color="transparent">
<template v-slot:activator="{ on, attrs }">
<div
v-on="item[itemText].length >36?on:null"
class="combobox-item comboboxOverFlow"
:data-testid="itemDataTestIdPrefix+item.id"
:parent-list="dataTestId">
<span>{{ item[itemText] }}</span>
</div>
</template>
<div class="popup-rectangle">
<span>{{ item[itemText] }}</span>
</div>
</v-tooltip>
</template>
</v-autocomplete>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: [],
},
labelKey: {
type: String,
default: '',
},
itemText: {
type: String,
default: '',
},
itemValue: {
type: String,
default: '',
},
value: {
type: [Number, String, Array],
},
disabled: {
type: Boolean,
default: false,
}
shouldTriggerWatch: {
type: Boolean,
default: false,
}
},
data() {
return {
selected: this.value,
showSearchIcon: false,
};
},
watch: {
value: {
immediate: true,
handler(val) {
if (this.shouldTriggerWatch) {
this.selected = val;
this.$emit('input', this.selected);
}
},
},
},
computed: {
localItems() {
if (this.items && this.items.length) {
return this.items.map(x => ({
...x,
displayName: x.lang_key ? this.$t(x.lang_key) : x.name,
}));
}
return [];
},
},
methods: {
onListItemSelected() {
this.$emit('input', this.selected);
},
autoCompleteFilter(item, queryText, itemText) {
const re = new RegExp(`(\\s+|^)(${queryText.toLocaleLowerCase()})(.*|$)`);
return re.test(itemText.toLocaleLowerCase());
},
},
};
I want to test three test case.
When I pass empty list should render as empty dropdown and render no-data slot.
<template v-slot:no-data>
<v-list-item id="noMatchText">
<v-list-item-title>
<div :no-data-item="dataTestId">
{{ $t('selection.noMatchFound') }}
</div>
</v-list-item-title>
</v-list-item>
</template>
When I pass filled list with test data I want to test all items rendered correctly
<template v-slot:item="{ item }">
<v-tooltip top color="transparent">
<template v-slot:activator="{ on, attrs }">
<div
v-on="item[itemText].length >36?on:null"
class="combobox-item comboboxOverFlow"
:data-testid="itemDataTestIdPrefix+item.id"
:parent-list="dataTestId">
<span>{{ item[itemText] }}</span>
</div>
</template>
<div class="popup-rectangle">
<span>{{ item[itemText] }}</span>
</div>
</v-tooltip>
</template>
When I search I want to provide my filter function works as expected. And autocomplete renders items according to filtered value.
This is what my test file.
function mountComponent(options) {
return mount(ComboBox, {
vuetify: new Vuetify(),
sync: false,
mocks: {
$t: key => key,
$i18n: { locale: 'en' },
},
...options,
});
}
describe('Combobox unit tests', () => {
beforeEach(() => {
document.body.setAttribute('data-app', 'true');
});
test('should create component successfully', () => {
const wrapper = mountComponent({ items: [] });
expect(wrapper.exists()).toBeTruthy();
});
test('should list zero items if the item list is empty', async () => {
const wrapper = mountComponent({
propsData: {
items: [],
labelKey: 'labelKey',
dataTestId: 'test-dropdown',
itemText: 'name',
itemValue: 'id',
},
});
const autocomplete = wrapper.find('.product-combobox');
const autocompleteControls = autocomplete.find('.v-input__slot');
autocompleteControls.trigger('click');
await wrapper.vm.$nextTick();
await flushPromises();
**// v menu cant opened !!!!**
});
test('should list third items correctly', async () => {
const testItems = [{ name: 'item1', id: 1 }, { name: 'item2', id: 2 }, { name: 'item3', id: 3 }];
const wrapper = mountComponent({
attachToDocument: true,
propsData: {
eagerProp: true,
items: testItems,
dataTestId: 'test-dropdown',
itemDataTestIdPrefix: 'test-dropdown-item-',
itemText: 'name',
itemValue: 'id',
value: null,
},
});
const slot = wrapper.find('.v-input__slot');
const input = wrapper.find('input');
// Focus input should only focus
input.trigger('focus');
expect(wrapper.vm.$children[0].isFocused).toBe(true);
expect(wrapper.vm.$children[0].menuCanShow).toBe(true);
expect(wrapper.vm.$children[0].isMenuActive).toBe(false);
slot.trigger('click');
expect(wrapper.vm.$children[0].isMenuActive).toBe(true);
expect(wrapper.vm.$children[0].menuCanShow).toBe(true);
wrapper.setProps({ searchInput: 'foo' });
expect(wrapper.vm.$children[0].isMenuActive).toBe(true);
expect(wrapper.vm.$children[0].menuCanShow).toBe(true);
// v menu cant opened !!!!
// you think these expects is unnecesary. you are right I just explore this component
// if I success I'll delete all of them
});
test('should filter autocomplete search results', async () => {
});
});
I can't open v-menu in test enviroment.
I tried emit events and trigger('click')
When I console log wrapper.html() I cant see v menu is opened and all items rendered in html. Output is shown below.
<div class="comboContainer">
<div class="v-input product-combobox v-input--hide-details v-input--dense theme--light v-text-field v-text-field--enclosed v-text-field--outlined v-select v-autocomplete">
<div class="v-input__control">
<div role="combobox" aria-haspopup="listbox" aria-expanded="false" aria-owns="list-2" class="v-input__slot" style="height: 44px;">
<fieldset aria-hidden="true">
<legend style="width: 0px;"><span>​</span></legend>
</fieldset>
<div class="v-select__slot">
<label for="input-2" class="v-label theme--light" style="left: 0px; position: absolute;"><span></span></label><input data-testid="test-dropdown" id="input-2" type="text" autocomplete="off">
<div class="v-input__append-inner">
<div class="v-input__icon v-input__icon--append"><i aria-hidden="true" class="v-icon notranslate material-icons theme--light">$dropdownArrow</i></div>
</div>
<input type="hidden">
</div>
<div class="v-menu"></div>
</div>
</div>
</div>
</div>
My problem is where is items? <div class="v-menu"></div> Why I cant see in wrapper.html. I don't use shallowMount, I use mount. Because of this I cannot write my covered test cases.
How can I simulate opened menu and rendered all items and provide some assertions?
What am I missing?
Versions
"#nuxtjs/vuetify": "1.12.1",
"#vue/test-utils": "1.0.0-beta.29",
"vue-jest": "3.0.7"
Well, probably the better way would be mocking and substituting v-autocomplete with a test component (just control values that go to it and emit fake events from it). You are not developing Vuetify, so no need to test what happens inside a component.

Vuejs - v-bind.sync on resursive components (hierarchical list)

I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI
Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.