I am using vue-test-utils and jest for TDD. I've imported $store in vuejs route file(routes.js) and using store for some purposes. I've written a simple unit test for login page but when I run test, jest will throw the following error before even one of the testes run.
error:
TypeError: Cannot read property 'state' of undefined
130 | isAuth: true,
> 131 | layout: $store.state.dom.isMobile ? mobileSinglePage : panel
| ^
route.js:
import $store from "../store"
// --------- layouts
import panel from "../layout/panel";
import mobileSinglePage from "../layout/mobileSinglePage";
import MainLayout from "../layout/index";
import auth from "../layout/auth"
// transactions
import _transaction from "../pages/transaction/_transaction"
const routes = [
{
path: "",
component: MainLayout,
children: [
{
path: "",
redirect: "/panel/dashboard",
},
{
path: "login",
component: login,
name: "login",
meta: {
preventLoggedIn: true,
layout: auth
}
},
{
path: "/panel/transactions/:url",
name: "_transactions",
component: _transaction,
meta: {
isAuth: true,
layout: $store.state.dom.isMobile ? mobileSinglePage : panel
}
},
{
path: "*",
name: 'error_page',
redirect: '/404'
}
],
},
];
export default routes;
login.spec.js
import { mount, createLocalVue } from "@vue/test-utils"
import login from '@/pages/auth/login'
import Vuex from "vuex"
const localVue = createLocalVue()
localVue.use(Vuex)
describe("login", () => {
it("simple test", () => {
const wrapper = mount(login, {
localVue,
data(){
return {
form: {
mobile_number: '',
},
}
},
})
wrapper.find('#login').text()
})
})
Login.vue
<template>
<div class="w-100">
<transition>
<form @submit.prevent="requestOtp" class="w-100" v-if="step === 1">
<MobileInput
ref="mobile_number"
v-model="form.mobile_number"
autocomplete="false"
outlined
:counter="false"
label="mobile number"
/>
<AppButton
:disabled="!form.mobile_number || form.mobile_number.length !== 11"
type="submit"
color="secondary"
large
:loading="loading"
class="w-100 fontMobileMedium font0.875 loginBtn">
login
</AppButton>
</form>
</transition>
<transition>
<form @submit.prevent="verifyOtp" v-if="step === 2">
<AppPinCode
autofocus
dir="ltr"
:disabled="loading"
:length="6"
class="mb-6"
@complete="verifyOtp"
v-model="form.otpCode"/>
<AppButton
type="submit"
:loading="loading"
:disabled="!form.otpCode"
color="secondary"
large
class="w-100 fontMobileMedium font0.875 ">
login
</AppButton>
</form>
</transition>
</div>
</template>
<script>
import {mapActions} from "vuex"
import MobileInput from "../../components/inputs/MobileInput";
import AppPinCode from "../../components/inputs/AppPinCode";
import Mobile from "../../mixins/Mobile";
import {_setAuthData} from "../../services/utils"
export default {
name: "Login",
mixins: [Mobile],
data() {
return {
...mapActions('main', ['crud']),
form: {
mobile_number: '',
device_info: {
hardware: "web",
device_id: ""
},
otpId: null,
password: null,
otpCode: '',
},
step: 1,
countDownTimer: null,
loading: false,
showPass: false,
interValTime: null,
}
},
components: {
MobileInput,
AppPinCode
},
methods: {
async requestOtp() {
if (!this.form.mobile_number)
return
try {
this.loading = true
const otp = await this.crud({
action: 'post',
section: 'auth/otp',
data: {mobile_number: this.normalizeMobile(this.form.mobile_number)},
auth: false,
showError: true,
})
this.form.otpId = otp.data.id
} catch (e) {
this.step = 2
console.log(e)
} finally {
this.loading = false
}
},
async verifyOtp() {
if (!this.form.otpCode || !this.form.otpId)
return
try {
this.loading = true
const data = {
mobile_number: this.normalizeMobile(this.form.mobile_number),
otp: this.form.otpCode
}
const verify = await this.crud({
action: 'patch',
section: `auth/otp/${this.form.otpId}`,
data,
auth: false,
showError: true,
})
const {auth} = verify.data
await this.finalLogin(auth)
} catch (e) {
this.form.otpCode = ''
console.log(e)
} finally {
this.loading = false
}
},
async finalLogin(userData) {
const {access_token, refresh_token, expires_in, user} = userData
await _setAuthData({
access_token,
refresh_token,
expires_in,
profile: user
}, true)
this.$router.push({name: 'dashboard'})
},
},
}
</script>
store/dom.js(store module)
import breakPoints from "../assets/scss/breakpoints.scss"
export default {
namespaced: true,
state: {
isRtl: true,
isMobile: false,
isTablet: false,
breakpoints: {
sm: breakPoints.minSm.split("px")[0],
md: breakPoints.minMd.split("px")[0],
lg: breakPoints.minLg.split("px")[0],
xl: breakPoints.minXl.split("px")[0],
},
sizeToHideSidebar: breakPoints.minLg.split("px")[0],
mobileSize: breakPoints.minSm.split("px")[0],
tabletSize: breakPoints.minMd.split("px")[0],
AppWindowWidth: window.innerWidth,
AppWindowHeight: window.innerHeight,
},
mutations: {
SET(state, {key, value}) {
state[key] = value
},
DELETE(state, {key, type = 'string'}) {
switch (type) {
case 'array':
state[key] = []
break
case 'object':
state[key] = {}
break
case 'boolean':
state[key] = false
break
default:
state[key] = null
}
},
APPEND(state, {key, value}) {
// console.log(state)
if (typeof state[key] !== 'object')
return false
if (Array.isArray(state[key])) {
// console.log('also here')
state[key].push(value)
}
if (state[key] && !Array.isArray(state[key]) && state[key] !== null) {
// console.log(value)
state[key] = {...state[key], ...value}
// console.log(state)
}
}
},
actions: {
deviceDetectBySize({commit, state}) {
const width = window.innerWidth
commit('SET', {key: 'isTablet', value: width <= state.tabletSize})
commit('SET', {key: 'isMobile', value: width <= state.mobileSize})
},
windowSizeFn({dispatch}) {
dispatch('deviceDetectBySize')
window.addEventListener('resize', function (event) {
dispatch('deviceDetectBySize')
});
}
},
}
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import main from "./main"
import dom from "./dom"
Vue.use(Vuex)
const Store = new Vuex.Store({
modules: {
main,
dom,
},
strict: process.env.NODE_ENV === 'development'
})
export default Store
as you can see I've never used router object in my test file but there is error related to it.
So, your
Login.vue
uses both router (this.$router.push({name: 'dashboard'})
) and store (...mapActions('main', ['crud']),
), but you give neither on the mount.Maybe you should give it both:
See here and here for more details.
Also,
mapActions()
should be inmethods
rather thandata
(See here)