How do I mock ApolloMutation component? - testing

Everything about the app works fine except the testing.
I have tried to use mount and shallowMount methods exposed by #vue/test-utils to mock the ApolloMutation component.
I have seen couple of solutions on VueApollo testing. I have tried this. In my case, I'm using NuxtApollo community module. I tried to apply it the same way, but throwing error about the wrapper been empty, not sure if importing the #nuxtjs/apollo is the right thing to do.
import { createLocalVue, shallowMount } from '#vue/test-utils'
import VueApollo from '#nuxtjs/apollo'
import Register from '../pages/register/index.vue'
describe('Register user', () => {
let localVue
beforeEach(() => {
localVue = createLocalVue()
localVue.use(VueApollo, {})
})
it('Should click the register button to register', () => {
const mutate = jest.fn()
const wrapper = shallowMount(Register, {
localVue,
mocks: {
$apollo: {
mutate,
}
}
});
wrapper.find('.callMe').trigger('click')
expect(2+2).toBe(4)
});
});
The component I want to test.
<template>
<ApolloMutation
:mutation="require('../../apollo/mutations/createUser.gql')"
:variables="{firstName, lastName, username, phone, email, password}"
#done="onDone">
<template v-slot="{ mutate, loading, error }">
<input v-model="firstName" placeholder="first name">
<input v-model="lastName" placeholder="last name">
<input v-model="username" placeholder="username">
<input v-model="phone" placeholder="phone">
<input v-model="email" placeholder="email">
<input v-model="password" placeholder="password">
<button :disabled="loading" #click="mutate">CreateUser</button>
<button class="callMe" #click="callMe">CreateUser</button>
<p v-if="error">An error occurred: {{ error }}</p>
<p v-if="loading">loading...</p>
</template>
</ApolloMutation>
</template>
I expected the output to be 4 but I got the error: [vue-test-utils]: find did not return .callMe, cannot call trigger() on empty Wrapper

Should be import VueApollo from 'vue-apollo' not import VueApollo from '#nuxtjs/apollo'.
Replace that everything should be fine.

Related

What is proper Vue 3 <Setup Script/> syntax for Watchers?

What is proper Vue 3 syntax for Watchers? It seems to be omitted from the docs. I'm very excited about the new features seen here:
https://vuejs.org/guide/essentials/watchers.html#deep-watchers
But what's the intended syntax?
This is a basic example of a watch inside the script setup syntax:
<template>
<div>
<input type="text" v-model="user.name" placeholder="Name" />
<input type="text" v-model="user.lastname" placeholder="Lastname" />
{{ nameUpdatesCount }}
</div>
</template>
<script setup>
import { reactive, ref, watch } from "vue";
const user = reactive({ name: "", lastname: "" });
const nameUpdatesCount = ref(0);
const increaseNameUpdatesCount = () => {
nameUpdatesCount.value++;
};
watch(user, (newValue, oldValue) => {
console.log(newValue, oldValue);
increaseNameUpdatesCount();
});
</script>
In the example above, the watcher will be triggered every time that you write or delete something in the inputs. This happens because the name property changes its value. After that, the increaseNameUpdatesCount method is called, and you will see the nameUpdatesCount be incremented by one.

Testing with vitest and testing-library is not working: it is due to using the SFC Script Setup?

I'm new to Vue and especially with the composition functions. I'm trying to test a component that uses the script setup; however, it seems that it is not working.
The component is this one:
<template>
<el-card class="box-card" body-style="padding: 38px; text-align: center;" v-loading="loading">
<h3>Login</h3>
<hr class="container--separator">
<el-form ref="formRef"
:model="form"
>
<el-form-item label="Username">
<el-input v-model="form.username" placeholder="Username"/>
</el-form-item>
<el-form-item label="Password">
<el-input type="password" v-model="form.password" placeholder="Password" />
</el-form-item>
<el-button color="#2274A5" v-on:click="submitForm()">Login</el-button>
</el-form>
</el-card>
</template>
<script lang="ts" setup>
import {reactive, ref} from 'vue'
import { useRouter } from 'vue-router'
import type {FormInstance} from 'element-plus'
import {useMainStore} from "../../stores/index"
import notification from "#/utils/notification"
import type User from "#/types/User"
const formRef = ref<FormInstance>()
const form: User = reactive({
username: "",
password: "",
})
const router = useRouter()
const loading = ref(false)
const submitForm = (async() => {
const store = useMainStore()
if (form.username === "") {
return notification("The username is empty, please fill the field")
}
if (form.password === "") {
return notification("The password is empty, please fill the field")
}
loading.value = true;
await store.fetchUser(form.username, form.password);
loading.value = false;
router.push({ name: "home" })
})
</script>
<style lang="sass" scoped>
#import "./LoginCard.scss"
</style>
When I try to test it:
import { test } from 'vitest'
import {render, fireEvent} from '#testing-library/vue'
import { useRouter } from 'vue-router'
import LoginCard from '../LoginCard/LoginCard.vue'
test('login works', async () => {
render(LoginCard)
})
I had more lines but just testing to render the component gives me this error.
TypeError: Cannot read properties of undefined (reading 'deep')
❯ Module.withDirectives node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:3720:17
❯ Proxy._sfc_render src/components/LoginCard/LoginCard.vue:53:32
51| loading.value = false;
52|
53| router.push({ name: "home" });
I tried to comment parts of the component to see if it was an issue with a specific line (the router for example), but the problem seems to continue.
I tried to search about it but I don't know what I'm doing wrong, it is related to the component itself? Should I change how I've done the component?
I had the same issue, and was finally able to figure it out. Maybe this will help you.
The problem was I had to register global plugins used by my component when calling the render function.
I was trying to test a component that used a directive registered by a global plugin. In my case, it was maska, and I used the directive in a input that was rendered somewhere deeply nested inside my component, like so:
<!-- a global directive my component used -->
<input v-maska="myMask" .../>
#vue/test-utils didn't recognize it automatically, which caused the issue. To solve it, I had to pass the used plugin in a configuration parameter of the render() function:
import Maska from 'maska';
render(MyComponent, {
global: {
plugins: [Maska]
}
})
Then, the issue was gone. You can find more info about render()
configuration here:
https://test-utils.vuejs.org/api/#global

How to prevent Quasar q-form submit when q-field has validation error?

I'm trying to implement vue-phone-input by wrapping it with a Quasar q-field.
It's mostly working. The input works fine and it shows validation errors underneath the input.
The problem is that I can submit the form even if there is a validation error.
How do I prevent this from happening?
Normally when using a q-form with a q-input and q-btn it will automatically stop this from happening.
So why doesn't it work here with q-field and vue-tel-input?
<template>
<q-form #submit="handlePhoneSubmit">
<q-field
v-if="isEditingPhone"
autocomplete="tel"
label="Phone"
stack-label
:error="isPhoneError"
error-message="Please enter a valid phone number."
outlined
hide-bottom-space
>
<vue-tel-input
v-model="phoneInput"
#validate="isPhoneError = !isPhoneError"
></vue-tel-input>
</q-field>
<q-btn
color="primary"
text-color="white"
no-caps
unelevated
style="max-height: 56px"
type="submit"
label="Save"
#submit="isEditingPhone = false"
/>
</q-form>
</template>
<script setup lang="ts">
import { ref, Ref } from 'vue';
import { VueTelInput } from 'vue-tel-input';
import 'vue-tel-input/dist/vue-tel-input.css';
const phone: Ref<string | null> = ref('9999 999 999');
const isEditingPhone = ref(true);
const isPhoneError = ref(false);
const phoneInput: Ref<string | null> = ref(null);
const handlePhoneSubmit = () => {
phone.value = phoneInput.value;
console.log('Form Saved');
};
</script>
First, you should use the :rules system from Quasar instead of :error and #validate
<q-field :rules="[checkPhone]"
function checkphone(value: string) {
return // validate the value here
}
Then, if the submit doesn't suffice, you may need to set a ref on your <q-form, then call its validate() method.
Here how to do it (I removed parts of the code to highlight what's required).
<template>
<q-form ref="qform" #submit="handlePhoneSubmit">
//..
</q-form>
</template>
<script setup lang="ts">
import { QForm } from "quasar";
import { ref } from "vue";
//..
const qform = ref<QForm|null>(null);
async function handlePhoneSubmit() {
if (await qform.value?.validate()) {
phone.value = phoneInput.value;
}
}

Feathers vuex : user is no longer logged in after refresh

I am new to feathers and vue js I don't understand this, when i log in a user the v-if directives works on the navbar, but when i refresh the page i notice that the user is no longer logged in still the JWT is stored in the localStorage.
App-Navbar.vue
<template>
<div>
<q-header bordered class="bg-white">
<q-toolbar>
<div class="q-gutter-sm" v-if="!user">
<q-btn to="/login" />
<q-btn to="/signup" />
</div>
<div class="q-gutter-sm" v-if="user">
<q-btn #click="logout"/>
</div>
</q-toolbar>
</q-header>
</div>
</template>
<script>
import { mapActions, mapState } from "vuex";
export default {
methods: {
...mapActions("auth", { authLogout: "logout" }),
logout() {
this.authLogout().then(() => this.$router.push("/login"));
}
},
computed: {
...mapState("auth", { user: "payload" })
}
};
</script>
You just need some logic to fetch your token on refresh.
You will first need to save it somewhere, let's say sessionStorage.
You can use vuex-persistedstate as you mentioned, but a lighter solution would be just setting your user property in the state of the store like this
user: sessionStorage.getItem(yourkey) ?? null

Property or method "posts" is not defined in the instance in Windows 7

I made a strange problem. I have a code works perfectly in MacOS, but in Windows 7, it goes error: TypeError: Property or method "posts" is not defined in the instance.
Please take a look my code.
<template>
<div class="row flex">
<div class="col-md-2 left_nav">
<leftnav/>
</div>
<div class="col-md-6 home_feed">
<post-detail :posts="posts"></post-detail>
</div>
<div class="col-md-2 flex">
<explorecol/>
</div>
<chatcol/>
</div>
</template>
<script>
import axios from 'axios'
import leftnav from '~/components/newsfeed/Nav.vue'
import chatcol from '~/components/Chat_Col.vue'
import explorecol from '~/components/feeds/Explore_Col.vue'
import Item from '~/components/feeds/Post.vue'
export default {
async asyncData ({ route }) {
let { data } = await axios.get('http://localhost:8000/api/v1/feeds/' + route.params.id + '/')
return {
posts: data
}
},
components: {
leftnav,
chatcol,
explorecol,
'post-detail': Item
}
}
</script>
I've made errors since I've installed package called: Vue Router Module. I use NuxtJS.