I'm writing a unit test with vue 3 using vee-validate 4 and Jest. But I'm new to this and I'm stuck at one place.
I have a TextInput component that I use validations, and when I call this component, I do validation when submit is done.
There is a separate component where I write the form where I use these textInputs.
First let me show you the code in my TkTextInput component.
<template>
<div
class="tk-Input"
:class="{ 'has-error': !!errorMessage, success: meta.valid }"
>
<label class="tk-label" :for="name">{{ label }}</label>
<input
id="demos"
class="col-12"
v-model="inputValue"
:name="name"
:type="type"
:value="inputValue"
:placeholder="placeholder"
#input="handleChange"
#blur="handleBlur"
v-bind="$attrs"
/>
<p class="help-message" v-show="errorMessage || meta.valid">
{{ errorMessage }}
</p>
</div>
</template>
<script>
import {useField} from "vee-validate";
import {watch} from "vue";
export default {
props: {
type: {
type: String,
default: "text",
},
modelValue: String,
value: {
type: String,
default: "",
},
name: {
type: String,
required: true,
},
label: {
type: String,
},
placeholder: {
type: String,
default: "",
},
},
emits: ['update:modelValue'],
setup(props, {emit}) {
const {
value: inputValue,
errorMessage,
handleBlur,
handleChange,
meta,
} = useField(props.name, undefined, {
initialValue: props.value,
});
watch(inputValue, (val) => {
emit('update:modelValue', val);
});
watch(() => props.modelValue, (val) => {
if (val !== inputValue.value) {
inputValue.value = val;
}
})
return {
handleChange,
handleBlur,
errorMessage,
meta,
inputValue,
};
},
};
</script>
Then in my form component where I call these textInputs is as follows.
<Form
#submit="onSubmit"
:validation-schema="schema">
<div class="grid ">
<div class="col-12 lg:col-6 lg:mb-0">
<tk-text-input v-model.trim="vehicleInfo.Plate" label="Plaka*" name="Plate" type="text"/>
</div>
<div class="grid ">
<div class="col-12 lg:col-6 lg:mb-0">
<tk-text-input v-model.trim="vehicleInfo.PhoneNumber" label="Cep Telefonu*" name="PhoneNumber"/>
</div>
<div class="col-12 lg:col-6 lg:mb-0">
</div>
</div>
<Button #click="clicked" class=" p-button-success" type="submit">{{buttonLabel}}</Button>
</Form>
Now I want to test the validation process in the TkTextInput component when the button clicks when I click the button in the component where I wrote the form with gesture. But I couldn't do it.
The test I wrote in the .spec file is as follows.
describe('TkTextInput.vue', () => {
it('when validation is done', async() => {
const wrapperVehicle = mount(VehicleInfoDialog, {
global:{
plugins: [PrimeVue]
},
})
const button = wrapperVehicle.find("button")
button.trigger('submit')
await button.trigger("click")
expect(wrapperVehicle.html()).toContain("Boş Geçilemez.")
});
})
Related
I want to validate using vue vee-validate and baseinputs. But I cannot use v-model where I call the component.
my TextInput Component.
<template>
<div
class="TextInput"
:class="{ 'has-error': !!errorMessage, success: meta.valid }"
>
<label :for="name">{{ label }}</label>
<input
:name="name"
:id="name"
:type="type"
:value="inputValue"
:placeholder="placeholder"
#input="handleChange"
#blur="handleBlur"
/>
<p class="help-message" v-show="errorMessage || meta.valid">
{{ errorMessage || successMessage }}
</p>
</div>
</template>
<script>
import { useField } from "vee-validate";
export default {
props: {
type: {
type: String,
default: "text",
},
value: {
type: String,
default: "",
},
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
successMessage: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
},
setup(props) {
const {
value: inputValue,
errorMessage,
handleBlur,
handleChange,
meta,
} = useField(props.name, undefined, {
initialValue: props.value,
});
return {
handleChange,
handleBlur,
errorMessage,
inputValue,
meta,
};
},
};
App.vue file where I call the component
<template>
<div>
{{ test }}
<Form
#submit="onSubmit"
:validation-schema="schema"
#invalid-submit="onInvalidSubmit"
>
<TextInput
name="name"
type="text"
label="Full Name"
placeholder="Your Name"
success-message="Nice to meet you!"
v-model="test"
/>
<button class="submit-btn" type="submit">Submit</button>
</Form>
</div>
</template>
<script>
import { ref } from "vue";
import { Form } from "vee-validate";
import * as Yup from "yup";
import TextInput from "./components/TextInput.vue";
export default {
name: "App",
components: {
TextInput,
Form,
},
setup() {
const test = ref("asda");
function onSubmit(values) {
alert(JSON.stringify(values, null, 2));
}
function onInvalidSubmit() {
const submitBtn = document.querySelector(".submit-btn");
submitBtn.classList.add("invalid");
setTimeout(() => {
submitBtn.classList.remove("invalid");
}, 1000);
}
const schema = Yup.object().shape({
name: Yup.string().required(),
email: Yup.string().email().required(),
password: Yup.string().min(6).required(),
confirm_password: Yup.string()
.required()
.oneOf([Yup.ref("password")], "Passwords do not match"),
});
return {
onSubmit,
schema,
onInvalidSubmit,
test,
};
},
};
</script>
where I wrote in v-model ="test" doesn't work. I tried to send it with emit but I couldn't.
So, in summary, let the component I validate perform the validation process, but I can use the v-model where I call the component.
I have this simple registration page:
<template>
<div class="login">
<div class="login-content">
<h1 #click="redirect('/')">Logo</h1>
</div>
<div class="login-header">
<p class="paragraph-small right">Already have account?
<span class="paragraph-small pointer link" #click="redirect('/login')">Log in!</span>
</p>
</div>
<div class="login-inputs">
<div class="login-inputs-container">
<h1>Sign up</h1>
<Input :error="error" :title="'Email'" :type="'email'" :value="email" />
<Input :error="error" :title="'Password'" :type="'password'" :value="password" />
<Input :error="error" :title="'Repeat password'" :type="'password'" :styles="'padding-bottom: 10px'" :value="passwordRepeat" />
<Checkbox :value="tac" :label="`I have read and accepted <a href='/'>terms and conditions.</a>`" />
<Button :label="'Sign up'" :clickon="register" />
<p v-if="error">Passwords have to match!</p>
</div>
</div>
</div>
</template>
<script>
import { register } from "~/api";
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex';
import Input from "~/components/Input";
import Button from "~/components/Button";
import Checkbox from "~/components/Checkbox";
export default {
name: "register",
components: {
Input,
Button,
Checkbox
},
watch: {
password() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) },
passwordRepeat() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) }
},
computed: {
...mapGetters({
email: 'register/getEmail',
password: 'register/getPassword',
passwordRepeat: 'register/getPasswordRepeat',
status: 'register/getStatus',
error: 'register/getError',
tac: 'register/getTac'
})
},
methods: {
redirect(path) {
this.$router.push({ path })
},
async register() {
console.log(this.tac, this.password, this.passwordRepeat, this.email)
}
}
}
</script>
<style lang="scss">
#import "../assets/css/login";
</style>
As you can see, there are 4 fields where I want to change value - 3 Input and 1 Checkbox. When I provide data and click button in console I get the default values, I was trying to do something with mutations and actions, but it doesn't work.
Can it be because I use my components, not default?
Also, here is my store store/register.js
export const state = () => ({
email: null,
password: null,
passwordRepeat: null,
status: null,
error: false,
tac: false
})
export const mutations = {
setEmail(state, value) { state.email = value },
setPassword(state, value) { state.password = value },
setPasswordRepeat(state, value) { state.passwordRepeat = value },
setStatus(state, value) { state.status = value },
setError(state, value) { state.error = value },
setTac(state, value) { state.tac = value }
}
export const actions = {
fetchEmail(ctx, value) { ctx.commit('setEmail', value) },
fetchPassword(ctx, value) { ctx.commit('setPassword', value) },
fetchPasswordRepeat(ctx, value) { ctx.commit('setPasswordRepeat', value) },
fetchStatus(ctx, value) { ctx.commit('setStatus', value) },
fetchError(ctx, value) { ctx.commit('setError', value) },
fetchTac(ctx, value) { ctx.commit('setTac', value) },
}
export const getters = {
getEmail(state) { return state.email },
getPassword(state) { return state.password },
getPasswordRepeat(state) { return state.passwordRepeat },
getStatus(state) { return state.status },
getError(state) { return state.error },
getTac(state) { return state.tac },
}
If problem is that I use not default tags, but my components with props, here is Checkbox component:
<template>
<div class="checkbox-container">
<label class="container">
<input type="checkbox" :value="innerValue" #input="onInput">
<span class="checkmark"></span>
</label>
<p class="checkbox-paragraph" v-html="label" />
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: ''
},
value: {
type: Boolean,
default: false
}
},
name: "Checkbox",
watch: {
value(value) {
this.innerValue = value
},
innerValue(value) {
this.$emit('input', value)
}
},
data() {
return {
innerValue: this.value
}
},
methods: {
onInput() {
this.$nextTick(() => {
this.innerValue = !this.innerValue
})
}
}
}
</script>
One way that can help you change the value of your checkbox is like this.
Checkbox Component:
<template>
<div class="checkbox-container">
<label class="container">
<input type="checkbox" #change="$emit('checkbox', value)" />
<span class="checkmark"></span>
</label>
</div>
</template>
<script>
export default {
name: 'Checkbox',
data() {
return {
value: false,
}
},
}
</script>
Now inside your register page you can use the checkbox component in template like this:
<Checkbox #checkbox="checkboxChanged" />
Now in the same page and in method section add this method:
checkboxChanged(event) {
this.$store.dispatch('register/fetchTac', event)
},
},
This way, when the value of checkbox changes you can have the changed value in your store too and get it with mapGetter. You can do the same to your inputs.
Okay, here is my working answer, I don't really know if it's correct, but it doesn't contain any errors or warnings:
<template>
<div class="login">
<div class="login-content">
<h1 #click="redirect('/')">Logo</h1>
</div>
<div class="login-header">
<p class="paragraph-small right">Already have account?
<span class="paragraph-small pointer link" #click="redirect('/login')">Log in!</span>
</p>
</div>
<div class="login-inputs">
<div class="login-inputs-container">
<h1>Sign up</h1>
<Input :error="error" :title="'Email'" :type="'email'" v-model="email" />
<Input :error="error" :title="'Password'" :type="'password'" v-model="password" />
<Input :error="error" :title="'Repeat password'" :type="'password'" :styles="'padding-bottom: 10px'" v-model="passwordRepeat" />
<Checkbox v-model="tac" :label="`I have read and accepted <a href='/'>terms and conditions.</a>`" />
<Button :label="'Sign up'" :clickon="register" />
<p v-if="error">Passwords have to match!</p>
</div>
</div>
</div>
</template>
<script>
import { register } from "~/api";
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex';
import Input from "~/components/Input";
import Button from "~/components/Button";
import Checkbox from "~/components/Checkbox";
export default {
name: "register",
components: {
Input,
Button,
Checkbox
},
watch: {
...mapActions(['fetchTac', 'fetchError', 'fetchStatus', 'fetchPasswordRepeat', 'fetchPassword', 'fetchEmail']),
password() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) },
passwordRepeat() { this.error = (this.password !== this.passwordRepeat) && (this.password !== null && this.passwordRepeat !== null) }
},
computed: mapGetters(['getError', 'getEmail', 'getPassword', 'getPasswordRepeat', 'getStatus', 'getTac']),
data() {
return {
email: null,
password: null,
passwordRepeat: null,
status: null,
error: false,
tac: false
}
},
methods: {
redirect(path) {
this.$router.push({ path })
},
async register() {
console.log(this.passwordRepeat, this.password, this.email, this.tac)
}
}
}
</script>
But I still have one problem, as you can see, I have getters and data at the same time, I can actually remove data, but it will cause such warning:
Property or method "propname" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property
It will work, but I will have this warning.
EDIT
I solved this problem that way:
computed: {
error: {
get() { return this.$store.getters.getError },
set(value) { this.$store.commit('setError', value) }
},
email: {
get() { return this.$store.getters.getEmail },
set(value) { this.$store.commit('setEmail', value) }
},
password: {
get() { return this.$store.getters.getPassword },
set(value) { this.$store.commit('setPassword', value) }
},
passwordRepeat: {
get() { return this.$store.getters.getPasswordRepeat },
set(value) { this.$store.commit('setPasswordRepeat', value) }
},
status: {
get() { return this.$store.getters.getStatus },
set(value) { this.$store.commit('setStatus', value) }
},
tac: {
get() { return this.$store.getters.getError },
set(value) { this.$store.commit('setTac', value) }
}
},
// data() {
// return {
// email: null,
// password: null,
// passwordRepeat: null,
// status: null,
// error: false,
// tac: false
// }
// },
I have a checkbox component in Vue:
<template>
<div class="checkbox">
<input class="checkbox-input" name="input" type="checkbox" v-model="checkbox">
</div>
</template>
<script>
export default {
data(){
return {
checkbox: false
};
},
};
</script>
So in the parent component I want to control these checkbox. So here is my parent component:
<div class="card">
<div class="card-header">
<CheckBox />
</div>
<div class="card-body" v-for="item in cart" :key="item.product.id">
<div class="checkbox-area">
<CheckBox />
</div>
</div>
So checkbox in card-body can be added when user clicks to add. So if a user clicks 3 times, 3 checkbox are being added inside of card-body. What I am trying to achieve is, as you see in card-header there is another checkbox, and when this checkbox is clicked, I want to check all the checkboxes inside card-body, and when it is unchecked in card-header, I want to unchcecked everything inside card-body.
So do you have any idea how to achieve this?
Thanks...
You can try like this :
Vue.component('checkbox', {
template: `
<div class="checkbox">
<input class="checkbox-input" name="input" type="checkbox" #change="getCheck" v-model="value">
</div>
`,
props: {
checked: {
type: Boolean,
default: false
}
},
data(){
return {
value: this.checked
};
},
methods: {
getCheck() {
this.$emit("set-checked", this.value)
}
},
watch: {
checked(){
this.value = this.checked
}
}
})
new Vue({
el: '#demo',
data(){
return {
all: false,
cart: [
{id: 1, check: false},
{id: 2, check: false},
{id: 3, check: true},
{id: 4, check: false}
]
};
},
watch: {
cart() {
this.cart.find(c => c.check === false) ? this.all = false : this.all = true
}
},
methods: {
checkAll(val) {
this.all = val
this.cart = this.cart.map(c => {
c.check = val
return c
})
},
checkItem(id) {
this.cart = this.cart.map(c => {
if(c.id === id) {
c.check = !c.check
}
return c
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div class="card">
<div class="card-header">
<p>All</p>
<checkbox :checked="all" #set-checked="checkAll" />
</div>
<br/>
<div class="card-body" v-for="item in cart" :key="item.id">
<div class="checkbox-area">
<checkbox :checked="item.check" #set-checked="checkItem(item.id)" />
</div>
</div>
</div>
</div>
First of all you need to add some props to your Component. than a wachter to emit a sync when the Value of the checkbox changes.
<template>
<div class="checkbox">
<input class="checkbox-input" name="input" type="checkbox" v-model="value">
</div>
</template>
<script>
export default {
props: {
checked: {
type: Boolean,
default: false
}
}
data(){
return {
value: this.checked
};
},
watch: {
value(){
this.$emit("update:checked", this.value)
}
}
};
</script>
on the cart you need to watch for theses changes an than you can check/uncheck all the items.
<template>
<div class="card">
<div class="card-header">
<CheckBox :checked.sync="global"/>
</div>
<div class="card-body" v-for="item in cart" :key="item.product.id">
<div class="checkbox-area">
<CheckBox :checked.sync="item"/>
</div>
</div>
</div>
</template>
<script>
export default {
data(){
return {
global: false,
cart: [
false,
false,
false
]
};
},
watch: {
global(){
for (let i = 0; i < this.cart.length; i++) {
this.chart[i] = this.global
}
}
}
};
</script>
I have not tested this code, but this should work...
Using checkbox input as custom components can be a little tricky see if this code can help you:
code sandbox
Vue.component('checkbox', {
template: `
<label>
<input type="checkbox" :value="value" v-model="deltaValue" />
{{ label }}
</label>
`,
name: "checkbox",
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
props: {
label: String,
value: [Object, Boolean, Array],
modelValue: {
type: [Array, Boolean],
default: () => [],
},
},
computed: {
deltaValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
},
},
},
});
new Vue({
el: '#app',
data() {
return {
areAllSelected: false,
checkedItems: [],
cart: [
{ id: 1, name: "tablet"},
{ id: 2, name: "phone"},
{ id: 3, name: "PC" },
],
};
},
watch: {
areAllSelected(areAllSelected) {
if (areAllSelected) {
this.checkedItems = [...this.cart];
return;
}
this.checkedItems = [];
},
checkedItems(items){
if (items.length === this.cart.length) {
this.areAllSelected = true;
return;
}
if (items.length === 0) {
this.areAllSelected = false;
}
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="card">
{{ checkedItems }}
<div class="card-header">
<checkbox label="check all" v-model="areAllSelected" />
</div>
<hr />
<div class="card-body" v-for="product in cart" :key="product.id">
<checkbox
:label="product.name"
:value="product"
v-model="checkedItems"
/>
</div>
</div>
</div>
Now I am writing a Google Chrome Extension that using vue el-input to input the username and password, when input username and password, store the username and password into storage.the code like this:
<template>
<div class="login">
<div class="title">Login</div>
<div class="setting-name">username:</div>
<div class="setting-input">
<el-input #change="saveConfig" placeholder="please input username" ></el-input>
</div>
<div class="setting-name">password:</div>
<div class="setting-input">
<el-input #change="saveConfig" placeholder="please input password"></el-input>
</div>
</div>
</template>
but when I type the username and password in the UI, the el-input did not show what I am inputting. It means could not input anything. what should I do to fix it? The vue version is:
"vue": "2.6.12",
"vue-loader": "15.9.5",
"vue-template-compiler": "2.6.12",
I have tried to forceUpdate like this:
<el-input #change="saveConfig" placeholder="please input username" #input="onInput()"></el-input>
methods: {
onInput(){
this.$forceUpdate();
},
}
still not work. This is my full code:
<template>
<div class="login">
<div class="title">登录</div>
<div class="setting-name">用户名:</div>
<div class="setting-input">
<el-input #change="saveConfig" placeholder="请输入你的用户名" #input="onInput()"></el-input>
</div>
<div class="setting-name">密码:</div>
<div class="setting-input">
<el-input #change="saveConfig" placeholder="请输入你的密码"></el-input>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
data: () => ({
loading: true,
defaultConfig,
config: defaultConfig,
time: '',
leftTime: '',
second: 0,
refreshDisabled: false,
isChrome: navigator.userAgent.indexOf('Chrome') !== -1,
}),
methods: {
onInput(){
this.$forceUpdate();
},
saveConfig() {
saveConfig(this.config, () => {
this.$message({
message: '保存成功',
type: 'success'
});
});
},
toHotkey() {
chrome.tabs.create({
url: 'chrome://extensions/shortcuts'
});
},
refreshRu() {
this.refreshDisabled = true;
refreshRules(() => {
this.second = 0;
this.time = secondToTime(this.second);
this.leftTime = secondToTime(this.config.refreshTimeout - this.second);
this.refreshDisabled = false;
});
},
refreshTime() {
getRulesDate((date) => {
this.second = (+new Date - +date) / 1000;
this.time = secondToTime(this.second);
this.leftTime = secondToTime(this.config.refreshTimeout - this.second);
setTimeout(() => {
this.refreshTime();
}, 1000);
});
}
}
}
</script>
I'm a newbie of Vue, and I'm trying to simply clear the data of input component once I've submitted, but it seems I'm missing something, because since it's parent data is cleared, I still see the filled value of the input component.
Here is a living example.
I've set to the input child component v-model="title" from it's parent wrapper. Once I submit the data to the parent, I call addItem and in the end, I supposed to clear the data model just by clear it this.title = '', but I probably do something wrong on how to bind data from parent to child.
And above the code, starting from the parent component:
<template>
<form #submit="addItem" class="todo-insert">
<input-item
icon="create"
name="title"
placeholder="Add a ToVue item..."
v-model="title"
/>
<button-item tone="confirm" class="todo-insert__action">
Aggiungi
</button-item>
</form>
</template>
<script>
import ButtonItem from '#vue/Form/ButtonItem/ButtonItem.vue'
import InputItem from '#vue/Form/InputItem/InputItem.vue'
import uuid from 'uuid'
export default {
name: 'TodoInsert',
components: {
ButtonItem,
InputItem
},
data () {
return {
title: ''
}
},
methods: {
addItem (e) {
e.preventDefault()
const todo = {
id: uuid.v4(),
isComplete: false,
title: this.title
}
this.$emit('add-todo', todo)
this.title = ''
}
}
}
</script>
<style lang="scss" scoped src="./TodoList.scss"></style>
This is the child input component:
<template lang="html">
<label class="input">
<div v-if="label" class="input__label text-sans text-sans--label">
{{ label }}
</div>
<div class="input__row">
<input
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
<div v-if="placeholderLabel" class="input__placeholder text-sans text-sans--placeholder">
{{ placeholderLabel }}
</div>
<div v-if="icon" class="input__icon-area">
<icon-item
:name="icon"
/>
</div>
</div>
</label>
</template>
<script>
import IconItem from '../../IconItem/IconItem.vue'
export default {
name: 'InputItem',
props: {
autocomplete: {
type: String,
default: 'off'
},
icon: String,
label: String,
name: {
type: String,
default: 'input-text'
},
placeholder: String,
placeholderLabel: String
},
computed: {
hasPlaceholderLabel () {
return this.placeholderLabel ? 'input__field--placeholder-label' : ''
},
isDirty () {
// return this.$attrs.value ? 'input__field--dirty' : ''
return 'input__field--dirty'
}
},
methods: {
updateValue: function (value) {
this.$emit('input', value)
}
},
components: {
IconItem
}
}
</script>
<style lang="scss" src="./InputItem.scss"></style>
What am I missing?
Your child component is bound unidirectionally. It means that it can change the value, but does not receive any update from the parent component. To receive updates, you need to receive the property value in your child:
props: {
value: String
}
Then, you need to pass the value received to the input :
<input
:value="value"
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
Now the input should update when the parent component changes the value