Vue test-utils for bootstrap form inputs - vue.js

I am writing a test case for my form component that incorporates custom inputs such as b-form-input, the Model Select component from the vue-search-select library, and the vuelidate validator. I encountered an issue at the outset of my efforts, specifically in regards to setting values in the input fields. I encountered the errors, setValue cannot be called on the selected element and Selecting an empty wrap.
Packages:
"vue": "^2.6.14",
"bootstrap": "^4.6.1",
"bootstrap-vue": "^2.21.2",
"nuxt": "^2.15.8",
"vuelidate": "^0.7.7",
"vue-search-select": "^2.9.5",
The following is a code snippet of my form component:
<template>
<b-form ref="project_form" #submit.prevent="submitForm">
<b-form-group id="name" label="Name" label-for="name">
<b-form-input
ref="name"
id="name"
:class="{
'is-invalid': $v.formData_project.name.$error,
'is-valid': !$v.formData_project.name.$invalid,
}"
placeholder="Project Name"
v-model="formData_project.name"
class="name col-md-12"
>
</b-form-input>
<div class="invalid-feedback" v-if="!$v.formData_project.name.required">
name is required
</div>
<div
class="invalid-feedback"
v-if="
!$v.formData_project.name.isUnique &&
$v.formData_project.name.required
"
>
name already existed
</div>
<div v-if="!$v.formData_project.name.maxLength">
Name should be less than 30 characters
</div>
</b-form-group>
<model-select
:options="options"
v-model="formData_project.country"
placeholder="select item"
id="country"
:class="{
'is-invalid': $v.formData_project.country.$error,
'is-valid': !$v.formData_project.country.$invalid,
}"
>
</model-select>
</template>
and for the test:
import axios from "axios";
import { mount } from "#vue/test-utils";
import createProject from "../components/site-components/modals/createProject.vue";
import { BForm } from 'bootstrap-vue'
import Vue from "vue";
Vue.component('b-form', BForm)
Vue.component('b-form-group',BForm)
Vue.component('b-form-input',BForm)
Vue.component('b-form-textarea',BForm)
Vue.component('b-button',BForm)
jest.mock("axios");
describe("Form", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(createProject);//,{stubs: {BForm}}
const html = wrapper.html();
});
it("should make a POST request to the API when form is submitted", async () => {
// Fill in form inputs
// console.log(wrapper.findComponent({ ref: 'project_form' }))
// console.log(wrapper.find({ ref: 'name' }).find('#name'))
wrapper.find('input[id="name"]').setValue('test1')
wrapper.find("#latitude").setValue(50);
wrapper.find("#longitude").setValue(100);
wrapper.find("#country").setValue("United States");
wrapper.find("#company").setValue("Test Company");
// Mock success response from API
axios.post.mockResolvedValue({ data: { success: true } });
// Submit the form
wrapper.find("b-form").trigger("submit");
// Wait for next tick to allow axios call to be made
await wrapper.vm.$nextTick();
// Check that the API was called with the correct data
expect(axios.post).toHaveBeenCalledWith(
"http://127.0.0.1:5000/api/projects/",
{
name: "Test Project",
latitude: 50,
longitude: 100,
country: "United States",
company: "Test Company",
}
);
});
});
I also tried
describe("Form", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(createProject,{stubs: {BForm}});//
const html = wrapper.html();
});
it("should make a POST request to the API when form is submitted", async () => {
// Fill in form inputs
wrapper.find('#name').setValue('test1')
Thanks for answering in advance.
Similar topics that I have gone through:
https://github.com/vuejs/vue-test-utils/issues/1883
https://github.com/vuejs/vue-test-utils/issues/957
but none of the solutions solved my problem.

Related

Why stripe doesn't correctly redirect after payment with vue-stripe

I try to use stripe in a Laravel vuejs SPA.
I first installed vue-stripe with this command
npm i #vue-stripe/vue-stripe
Here is my component to trigger the payment
<template>
<div class="text-xl sass-editor-1 text-center">
<h1 class="text-2xl">Stripe Payment Gateway integration</h1>
<stripe-checkout
ref="checkoutRef"
mode="payment"
:pk="publishableKey"
:line-items="lineItems"
:sucess-url="successURL"
:cancel-url="cancelURL"
#loading="v =>loading = v"
/>
<button class="mt-4 p-2 text-white border-2 border-white rounded-lg bg-green-800" #click="submit">Pay now</button>
</div>
</template>
<script setup>
import {ref } from 'vue'
import {StripeCheckout} from '#vue-stripe/vue-stripe'
let publishableKey = "pk_test_51M6ZtzIWDjpHNQK16d1g0bq1L6wHgFxNg9KyuBiThC4fSXgAyUVjlwG6MFos0AaqaQYJOf2YC3a6oWlZqMjFtTZj00Tue51qVs"
let loading = ref(false);
let lineItems = ref();
lineItems.value = [
{
price: 'price_1M6qubIWDjpHNQ1rITHepQD',
quantity: 1
}
];
let successURL = ref(null);
successURL.value = 'http://localhost:3000/success';
let cancelURL = ref(null);
cancelURL.value = 'https://localhost:3000/error';
const checkoutRef = ref(null);
function submit() {
//stripe checkout page
checkoutRef.value.redirectToCheckout();
}
</script>
I also created pages for success an error that display a short message.
When I click the button, I am redirected of the stripe page to enter my credential and my card number.
After confirming the payment, I am not redirected to the success nor the error page but to the page that initiated the process, i.e. the page I describe here.
How comes the redirection doesn't work ?
P.S. the original script has been converted to the "script setup" form, but even with the classic form, the trouble is the same.
This is mine works perfectly try to change from successURL.value to just successURL.
refer to this documentation https://docs.vuestripe.com/vue-stripe/stripe-checkout/subscriptions and look on this page exactly to the code they also use just successURL and cancelURL no value.
<template>
<div>
<stripe-checkout
ref="checkoutRef"
mode="subscription"
:pk="publishableKey"
:line-items="lineItems"
:success-url="successURL"
:cancel-url="cancelURL"
#loading="v => loading = v"
/>
<button #click="submit">Subscribe!</button>
</div>
</template>
<script>
import { StripeCheckout } from '#vue-stripe/vue-stripe';
export default {
components: {
StripeCheckout,
},
data () {
return {
el:"checkoutRef",
publishableKey : import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY,
loading: false,
lineItems: [
{
price: 'price_1LgqdGALkwKTK48qE1lfox5G',
quantity: 1,
},
],
successURL: 'https://'+window.location.host+'/success',
cancelURL: 'https://'+window.location.host+'/cancel',
};
},
methods: {
submit () {
this.$refs.checkoutRef.redirectToCheckout();
},
},
};
</script>

How Do I correctly use vuex store?

I'm trying the following:
I have a login form that uses axios to get results from a rest api. When submitting the correct data I get returned all user data, when submitting the wrong data I get a error message.
I'd like to save the userdata to a global store to use within my vue application. And I figured that vuex would work for this szenario.
Within my main.js I defined vuex and a mutation function to get the user data.
import { createStore } from 'vuex'
// other imports
const store = createStore({
state () {
return {
userData: {}
}
},
mutations: {
addUserData (state, data) {
state.userData = data
}
}
})
createApp(App)
.use(router, store)
.mount('#app')
within my Login.vue I'm trying to use the store I created earlier:
However, when trying to use the mutation for the store addUserData I get Uncaught (in promise) ReferenceError: store is not defined in the console. How do I now correctly use the function?
I tried using this.$store.state.addUserData as well and end up with a different error
<template>
<div class="container content">
<h1>Login</h1>
<form #submit.prevent="login">
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="username" id="username" v-model="username" class="form-control" placeholder="Username">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password:</label>
<input type="password" id="password" v-model="password" class="form-control" placeholder="•••••••">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</template>
<script>
import axios from 'axios'
export default {
data(){
return {
username: '',
password: '',
}
},
methods: {
async login() {
axios.get('https://localhost:9443/isValidUser',
{
params: {
user: this.username,
pass: this.password,
}
}).then(response => {
console.log(response.data)
if(response.data[0].username) {
console.log("Hello " + response.data[0].username)
store.commit('addUserData', response.data[0])
} else {
console.log("Login failed")
}
})
}
}
}
</script>
My solution to use multiple plugins at "once" is that :
const app = createApp(App)
[store, router].forEach((p) => app.use(p));
app.mount('#app);
if you have a lot of plugins it can be handy, if not you can always do :
createApp(App)
.use(router)
.use(store)
.mount('#app')
for two plugins it's ok
So in the end your store is not used and hence the error

Correctly testing vee-validate validated form submit with Jest

I am trying to submit a form that uses vee-validate and test if the form calls the underlying store with Jest.
Here is my code:
Form:
<template>
<div class="flex flex-col justify-center h-screen bg-site-100">
<!-- Login body -->
<div class="container">
<div class="mx-auto w-4/12 p-7 bg-white">
<!-- Form -->
<Form id="loginForm" #submit="login" :validation-schema="schema" v-slot="{ errors }">
<div class="mt-4">
<div>
<text-box
:type="'email'"
:id="'email'"
:label="'Your Email'"
v-model="email"
:place-holder="'Email'"
:required="true"
:error="errors.email"
/>
</div>
<div>
<text-box
:type="'password'"
:id="'password'"
:label="'Parool'"
v-model="password"
:place-holder="'Password'"
:required="true"
:error="errors.password"
/>
</div>
<!-- Submit -->
<Button
type="submit"
id="loginButton"
:disabled="Object.keys(errors).length > 0"
class="text-white bg-site-600 w-full hover:bg-site-700 focus:ring-4 focus:ring-site-300 font-medium rounded-md text-sm px-5 py-2.5 mr-2 mb-2 focus:outline-none"
>
Log In
</Button>
</div>
</Form>
</div>
</div>
</div>
</template>
<script lang="ts">
import * as Yup from "yup";
import { Form } from "vee-validate";
import { defineComponent } from "vue";
import Button from "../core/Button.vue";
import TextBox from "../core/TextBox.vue";
import { mapActions, mapStores } from "pinia";
import { useAuthStore } from "../../store/auth";
import LoginDataType from "../../types/login_data";
export default defineComponent({
name: "Login",
components: { TextBox, Form, Button },
computed: { ...mapStores(useAuthStore) },
data() {
return {
email: "",
password: "",
schema: Yup.object().shape({
email: Yup.string().required("Email is required").email("Email is invalid"),
password: Yup.string().required("Password is required"),
}),
};
},
methods: {
async login() {
console.log("Logged in mock");
let data: LoginDataType = {
email: this.email,
password: this.password,
};
await this.authStore.login(data);
},
},
});
</script>
Store:
import { defineStore } from "pinia";
export const useAuthStore = defineStore("auth", {
state: () => ({
}),
getters: {
},
actions: {
async login(data: LoginDataType) {
// do something
},
}
})
Test:
it('logs in correctly when right username and password sent to API', async () => {
const store = useAuthStore();
jest.spyOn(store, 'login');
const wrapper = mount(Login, {
stubs: ['router-link']
});
const email = wrapper.find('input[id="email"]');
await email.setValue('testEmail#gmail.com');
// Check if model is set
expect(wrapper.vm.email).toBe(testEmail);
const password = wrapper.find('input[id="password"');
await password.setValue('testPw');
// Check if model is set
expect(wrapper.vm.password).toBe(testPw);
// Check form exists
const loginForm = wrapper.find('#loginForm');
expect(loginForm.exists()).toBe(true);
await loginForm.trigger('submit');
// Check if store method has been called
expect(store.login).toHaveBeenCalled();
expect(store.login).toHaveBeenCalledWith({
email: 'testEmail#gmail.com',
password: 'testPw'
})
});
The test fails at expect(store.login).toHaveBeenCalled(). Implying the form doesn't get submitted. The test works just fine when I replace the vee-validate component Form with a regular HTML form tag.
What might be causing this behaviour any help is highly appreciated? :)

Use more than one directive to add data attributes to components

I have two directives which are supposed to add data attributes to components for testing, however, only one of the directives actually gets added. The two components are Bootstrap-Vue's BFormInput and BButton.
I tried removing everything but one of the buttons and the directive is still not added i.e
<b-input-group class="sm-2 mb-2 mt-2">
<b-button
variant="primary"
#click="searchJobs"
class="rounded-0"
v-jobs-search-button-directive="{ id: 'search-button' }"
>
Search
</b-button>
</b-input-group>
wrapper.html() output is:
<b-input-group-stub tag="div" class="sm-2 mb-2 mt-2"><b-button-stub target="_self" event="click" routertag="a" variant="secondary" type="button" tag="button" class="rounded-0">
Search
</b-button-stub></b-input-group-stub>
However, it is added when instead of a button I leave in place the input form i.e.
<b-input-group class="sm-2 mb-2 mt-2">
<b-form-input
v-jobs-search-input-directive="{ id: 'input-keyword' }"
class="mr-2 rounded-0"
placeholder="Enter Search term..."
:value="this.searchConfig.Keyword"
#input="this.updateJobsSearchConfig"
/>
</b-input-group>
wrapper.html() output is:
<b-input-group-stub tag="div" class="sm-2 mb-2 mt-2"><b-form-input-stub value="" placeholder="Enter Search term..." type="text" class="mr-2 rounded-0" data-jobs-search-input-id="input-keyword"></b-form-input>
This is how I add the directives
<template>
<b-input-group class="sm-2 mb-2 mt-2">
<b-form-input
v-jobs-search-input-directive="{ id: 'input-keyword' }"
class="mr-2 rounded-0"
placeholder="Enter Search term..."
:value="this.searchConfig.Keyword"
#input="this.updateJobsSearchConfig"
/>
<b-button
variant="primary"
#click="searchJobs"
class="rounded-0"
v-jobs-search-button-directive="{ id: 'search-button' }"
>
Search
</b-button>
</b-input-group>
</template>
<script>
import { mapActions, mapState } from 'vuex'
import JobService from '#/api-services/job.service'
import JobsSearchInputDirective from '#/directives/components/jobs/JobsSearchInputDirective'
import JobsSearchButtonDirective from '#/directives/components/jobs/JobsSearchButtonDirective'
export default {
name: 'jobs-search',
directives: { JobsSearchInputDirective, JobsSearchButtonDirective },
data () {
return {
jobs: [],
pages: 0
}
},
computed: {
...mapState({
pagedConfig: state => state.jobs.paged,
searchConfig: state => state.jobs.search
})
},
methods: {
// Methods go here
}
}
jobs-search-input-directive is
export default (el, binding) => {
if (process.env.NODE_ENV === 'test') {
Object.keys(binding.value).forEach(value => {
el.setAttribute(`data-jobs-search-input-${value}`, binding.value[value])
})
}
}
jobs-search-button-directive is
export default (el, binding) => {
if (process.env.NODE_ENV === 'test') {
Object.keys(binding.value).forEach(value => {
el.setAttribute(`data-jobs-search-button-${value}`, binding.value[value])
})
}
}
This is the test I run, mounting with shallowMount
it('should call jobsSearch method on search button click event', () => {
wrapper.find('[data-jobs-search-button-id="search-button"]').trigger('click')
expect(searchJobs).toHaveBeenCalled()
})
which comes back with
Error: [vue-test-utils]: find did not return [data-jobs-search-button-id="search-button"], cannot call trigger() on empty Wrapper
However wrapper.find('[data-jobs-search-input-id="input-keyword"]') DOES find the input-form
The two directives are registered in the JobsSearch.vue component and they definitely get rendered if I remove the process.env part
I expect the attribute to be added to both components but it only gets added to the BFormInput when testing. Any help will be greatly appreciated.
I believe that the problem occurs when...
... trying to use a directive...
... on a functional child component...
... with shallowMount.
b-button is a functional component.
I've put together the demo below to illustrate the problem. It mounts the same component in 3 different ways and it only fails in the specific case outlined above.
MyComponent = {
template: `
<div>
<my-normal v-my-directive></my-normal>
<my-functional v-my-directive></my-functional>
</div>
`,
components: {
MyNormal: {
render: h => h('span', 'Normal')
},
MyFunctional: {
functional: true,
render: (h, context) => h('span', context.data, 'Functional')
}
},
directives: {
myDirective (el) {
el.setAttribute('name', 'Lisa')
}
}
}
const v = new Vue({
el: '#app',
components: {
MyComponent
}
})
document.getElementById('markup1').innerText = v.$el.innerHTML
const cmp1 = VueTestUtils.mount(MyComponent)
document.getElementById('markup2').innerText = cmp1.html()
const cmp2 = VueTestUtils.shallowMount(MyComponent)
document.getElementById('markup3').innerText = cmp2.html()
#markup1, #markup2, #markup3 {
border: 1px solid #777;
margin: 10px;
padding: 10px;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<script src="https://unpkg.com/vue-template-compiler#2.6.10/browser.js"></script>
<script src="https://unpkg.com/#vue/test-utils#1.0.0-beta.29/dist/vue-test-utils.iife.js"></script>
<div id="app">
<my-component></my-component>
</div>
<div id="markup1"></div>
<div id="markup2"></div>
<div id="markup3"></div>
I haven't really looked at the code for vue-test-utils before but stepping through in the debugger makes me suspicious of this line:
https://github.com/vuejs/vue-test-utils/blob/9dc90a3fd4818ff70e270568a2294b1d8aa2c3af/packages/create-instance/create-component-stubs.js#L99
This is the render function for the stubbed child component. It would appear that context.data.directives does contain the correct directive but they aren't being passed on in the call to h.
Contrast that with the render function in my example component MyFunctional, which passes on all of data. That's required for directives to work with a functional component but when MyFunctional gets replaced with a stub the new render function seems to drop the directives property.
The only workaround I've been able to come up with is to provide your own stub:
VueTestUtils.shallowMount(MyComponent, {
stubs: {
BButton: { render: h => h('div')}
}
})
By using a non-functional stub the directive works fine. Not sure how much value this would take away from the test though.

Veevalidate always return true Vuejs

I´m using webpack and instance VeeValidate using this way:
import VeeValidate from 'vee-validate';
Vue.use(VeeValidate, {
// This is the default
inject: true,
// Important to name this something other than 'fields'
fieldsBagName: 'veeFields'
});
I have a vuejs component created for the user to subscribe to the email. The problem is that this form always gives true when I use $validator.validateAll()
Have I not understood well the functioning of Vee-validate?
This is the code of my component newsletter.vue.js
Vue.component('newsletter', {
template : '<div>\
<b-form inline>\
<b-input v-validate required id="email" name="email" class="mb-2 mr-sm-2 mb-sm-0" placeholder="Deja tu email" type="email" :state="validate_input" />\
\
<b-button variant="primary-degree" #click="validateBeforeSubmit">Enviar</b-button>\
</b-form>\
</div>',
props : ['route_post'],
inject: ['$validator'],
data() {
return {
email: '',
}
},
computed: {
validate_input: function() {
return this.errors.has("email")
}
},
methods: {
onSubmit() {
// Form submit logic
},
validateBeforeSubmit() {
this.$validator.validateAll().then((result) => {
console.log(result);
if (result) {
// eslint-disable-next-line
alert('Form Submitted!');
return;
}
alert('Correct them errors!');
});
}
}
});
In order to add a validation of vee-validate you need to add it as value to v-validate option and not directly within the tag.
For more info check required example on docs
Update the below line in your code.
<b-input v-validate="'required'" id="email" name="email" class="mb-2 mr-sm-2 mb-sm-0" placeholder="Deja tu email" type="email" :state="validate_input" />
If you also want to display error you can add below line as =>
<span class="error" v-if="errors.has('email')">{{ errors.first('email') }}</span>