Vue unit tests failing because component method calls this.$route.query - TypeError: Cannot read property 'query' of undefined - vue.js

Long time user of the wisdom of StackOverflow on and off the job, but first time I'm posting a question. What a milestone!
I'm writing unit tests for a large Vue application, and one of my components uses a method that references $route in order to determine if a query param is being passed in / pass in the param if it is being used. The method calling this.$route.query.article_id works great, however now that I am in testing, the tests don't recognize this.$route.query
I've tried to mock the $route object when using shallowMount to mount my localVue, as described in the doc, but it doesn't work, and I continue to get the same error.
Here is my component:
<template>
<b-container fluid>
<div class="content-page-header"></div>
<b-row>
<b-col cols="3" class="outer-columns text-center" style="color:grey">
<font-awesome-icon
:icon="['fas', 'newspaper']"
class="fa-9x content-page-photo mb-3 circle-icon"
/>
<br />
<br />Get practical tips and helpful
<br />advice in clear articles written
<br />by our staff's experts.
</b-col>
<b-col cols="6" v-if="articlesExist">
<h1 class="header-text">
<b>Articles</b>
</h1>
<div v-if="!selectedArticle">
<div v-for="article in articles">
<article-card :article="article" #clicked="onClickRead" />
<br />
</div>
</div>
<div v-else>
<router-link to="articles" v-on:click.native="setSelectedArticle(null)">
<font-awesome-icon icon="chevron-circle-left" />&nbsp
<b>Back to All Articles</b>
</router-link>
<article-header :article="selectedArticle" />
<br />
<span v-html="selectedArticle.text"></span>
</div>
</b-col>
<b-col cols="6" v-else>
<h1 class="header-text">
<b>Articles</b>
</h1>
<div class="text-center">Stay tuned for more Articles</div>
</b-col>
<b-col class="outer-columns">
<b class="text-color" style="font-size:14pt">Saved Articles</b>
<div v-for="article in userArticles">
<router-link
:to="{path:'articles', query: {article_id: article.article.id}}"
v-on:click.native="setSelectedArticle(article.article)"
>
<user-article :article="article.article" />
</router-link>
<br />
</div>
</b-col>
</b-row>
</b-container>
</template>
<script>
import ArticleCard from "./ArticleCard";
import UserArticle from "./UserArticle";
import ArticleHeader from "./ArticleHeader";
import { library } from "#fortawesome/fontawesome-svg-core";
import {
faNewspaper,
faChevronCircleLeft
} from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
library.add(faNewspaper, faChevronCircleLeft);
export default {
name: "Articles",
props: [],
components: {
ArticleCard,
ArticleHeader,
UserArticle,
library,
FontAwesomeIcon,
faNewspaper,
faChevronCircleLeft
},
mixins: [],
data() {
return {
selectedArticle: null
};
},
computed: {
articles() {
return this.$store.getters.articles.filter(article => article.text);
},
articlesExist() {
return Array.isArray(this.articles) && this.articles.length;
},
userArticles() {
return this.$store.getters.userArticles;
},
articleParam() {
return parseInt(this.$route.query.article_id);
}
},
methods: {
setSelectedArticle(article) {
this.selectedArticle = article;
},
onClickRead(article) {
this.selectedArticle = article;
}
},
mounted() {
if (this.articleParam) {
this.setSelectedArticle(
this.articles.filter(article => article.id === this.articleParam)[0]
);
}
}
};
</script>
<style lang="stylus" scoped>
.text-color {
color: #549DB0;
}
.header-text {
color: white;
margin-top: -50px;
margin-bottom: 20px;
}
.outer-columns {
background-color: #F2FBFD;
padding-top: 20px;
}
.nav-back {
color: #549DB0;
background-color: #F0FBFD;
padding: 5px;
}
</style>
And here is my test:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue)
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})
As I mentioned, in the shallow mount, I've tried doing this:
const wrapper = shallowMount(Articles, {
store,
localVue,
mocks: {
$route: {
query: null
}
}
})
But I continue to get this error:
TypeError: Cannot read property 'query' of undefined
at VueComponent.articleParam (webpack-internal:///1:107:35)
When I remove the line return parseInt(this.$route.query.article_id); from the articleParam method, my test passes.
How do I get around this call to this.$route.query in the component? It's not necessary to my test, but is causing my test to fail when mounting the component.

import import VueRouter from 'vue-router'; in your unite test file and create a new object of the router like const router = new VueRouter(); and use it in your test case.
I have updated code here:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue);
const router = new VueRouter();
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
router,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})

Related

Vuelidate with i18n: Key not found in locale messages

So in my i18n-validators.js file I want to export validators with translated messages to my language of choice and use them in my vue component to validate a form.
My code:
// import * as VuelidateValidators from 'https://cdn.jsdelivr.net/npm/#vuelidate/validators';
// import * as VueI18n from 'https://unpkg.com/vue-i18n#9';
const messages = {
en: {
validations: {
required: 'The field {property} is required.',
}
},
cs: {
validations: {
required: 'Toto pole {property} je povinné',
}
},
}
const i18n = VueI18n.createI18n({
locale: 'cz',
fallbackLocale: 'en',
messages
})
const withI18nMessage = VuelidateValidators.createI18nMessage({
t: VueI18n.createI18n().global.t.bind(i18n)
})
export const required = withI18nMessage(VuelidateValidators.required)
Console:
Not found 'validations.required' key in 'en-US' locale messages. vue-i18n#9
Fall back to translate 'validations.required' key with 'en' locale. vue-i18n#9
Not found 'validations.required' key in 'en' locale messages.
And I want the validator to throw me the specified message instead of the "validations.required" message
First make sure you have installed vuelidade and vue-i18n
Following your example, you can change the file above to:
import * as validators from "#vuelidate/validators";
import { createI18n } from "vue-i18n";
const { createI18nMessage } = validators;
const messages = {
en: {
validations: {
required: "The field {property} is required.",
},
},
cs: {
validations: {
required: "Toto pole {property} je povinné",
},
},
};
const i18n = createI18n({
locale: "cs",
fallbackLocale: "en",
messages,
});
const withI18nMessage = createI18nMessage({ t: i18n.global.t.bind(i18n) });
export const required = withI18nMessage(validators.required);
as a component you can follow this one as example:
<template>
...
<div class="mb-3">
<input
v-model="formData.name"
className="form-control"
placeholder="Insert your name.."
/>
</div>
<span v-for="error in v$.name.$errors" :key="String(error.$uid)">
<span class="text-danger">{{ error.$message }}</span>
</span>
<div class="mt-5 submit">
<button class="btn btn-primary btn-sm" type="button" #click="submitForm">
Next
</button>
</div>
...
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
import useVuelidate from "#vuelidate/core";
import { required } from "#/utils/validators/i18n-validators";
export default defineComponent({
name: "InitialDataForm",
setup() {
const formData = reactive({
name: "",
});
const rules = {
name: { required },
};
const v$ = useVuelidate(rules, formData);
return {
formData,
v$,
};
},
methods: {
async submitForm() {
const result = await this.v$.$validate();
if (result) {
alert("validation passed");
}
},
},
});
</script>
and now you should be able to see the translated message:

How to change the content of webpage when I change the path of dynamic vue-router

I am using vue-router to write a dynamic router .and when I jump from one path to another, I find the path is indeed changed but the web page does not reload and I have no idea why.
the router definition is as follows:
const routes: Array<RouteRecordRaw> = [
{
path: '/lecture/:lecture_name',
name: 'Lecture',
component: LecturesBase,
}
]
and the dynamic web page vue components is like this:
LectureBase.vue
<template>
<div>
<a-layout>
<LectureNavigation />
<LectureTemplate
:page_name="page_name"
/>
<!-- <router-view>
</router-view> -->
<tui-juhe />
</a-layout>
</div>
</template>
<script>
import { ref } from 'vue';
import router from "#/router/index";
import { onBeforeRouteUpdate, useRoute } from "vue-router";
import LectureNavigation from "#/views/lecture/LectureNavigation.vue";
import LectureTemplate from "#/views/lecture/LectureTemplate.vue";
import TuiJuhe from "#/components/advertisement/TuiJuhe.vue";
export default {
name: "LectureBase",
setup() {
const route = useRoute();
},
components: {
LectureNavigation,
TuiJuhe,
LectureTemplate,
},
// props: {
// page_name: String, // 中文标题
// },
data() {
return {
$router: router,
page_name: ref(this.$route.params.lecture_name),
}
},
watch: {
'$route' (to, from) {
console.log('# to url: ' + to.path);
console.log('# to url: ' + this.$route.params.lecture_name);
this.page_name = this.$route.params.lecture_name;
console.log('# to url: ' + this.page_name);
this.$forceUpdate();
}
},
// methods: {
// refresh() {
// d
// }
// },
created() {
console.log("# url: " + this.$route.params.lecture_name);
}
};
</script>
<style>
</style>
LectureTemplate.vue
<template>
<a-layout class="variable_content" style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0" :routes="router">
<a-breadcrumb-item>
<router-link to="/">
<home-outlined />
</router-link>
</a-breadcrumb-item>
<a-breadcrumb-item>
<router-link to="/lecture/lecture_home_page">
NLP 教程
</router-link>
</a-breadcrumb-item>
<!--a-breadcrumb-item> Home </a-breadcrumb-item-->
<a-breadcrumb-item>{{ this.title }}</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content :style="{
background: '#fff',
padding: '24px',
margin: 0,
width: '100%',
minHeight: '280px',
}">
<div>
<h1><b class="b_green">{{ this.title }}</b></h1>
<p class="description_text">
发布日期:{{ this.established_time }} 阅读量:{{ this.frontend_page_count }}
</p>
<div v-html="markdownToHtml"></div>
</div>
</a-layout-content>
</a-layout>
</template>
<script>
import { useMeta } from 'vue-meta';
import router from "../../router/index";
import { stat_instance } from "#/utils/request";
import blog_asset from "#/utils/blog_request";
import {
HomeOutlined,
} from "#ant-design/icons-vue";
export default {
name: 'LectureTemplate',
components: {
HomeOutlined,
},
// setup() {
// useMeta({ title: this.title})
// },
props: {
page_name: String, // 英文名,用于请求后端,展示 url
},
data() {
return {
router: router,
title: '',
established_time: '',
frontend_page_count: 0,
markdown: "### loading ...",
}
},
computed: {
markdownToHtml() {
var markdown_content = this.md(this.markdown);
return markdown_content;
}
},
created() {
console.log("## temp url: " + this.page_name);
stat_instance({
url: "/stat_api/frontend_page_statistics",
data: {
page_name: this.page_name,
}
})
.then((response) => {
this.frontend_page_count = response.data.frontend_page_count;
this.title = response.data.title;
this.established_time = response.data.established_time;
})
.catch(() => {
this.frontend_page_count = 0;
});
blog_asset({
url: "/lecture/" + this.page_name + "/README.md",
})
.then((response) => {
this.markdown = response.data;
})
.catch(() => {
this.markdown = "### Failed to request markdown file.";
});
}
}
</script>
<style lang="less" scoped>
h1 {
width: 100%;
// height: 80px;
font-size: 28px;
padding-top: 5px;
margin: 10px;
}
.b_green {
color: #00B441;
}
.description_text {
text-align: right;
font-size: 10px;
color: #777777;
}
</style>
So, what is wrong with my code, and the complete project code is at JioNLP_online.
You could git clone this repository and execute npm run serve to try this code and check the bug.
The program indeed captured the dynamic path of the vue-router but the web page does not changed at all.
Solution I found
I found this post about the same issue here.
Basically you need to add the :key attribute to the <vue-router> component where your page is rendered.
Example:
// this re-renders the page when the path changes
<router-view :key="$route.fullPath"></router-view>
My solution
The :key solution didn't work for me, because I didn't want the router to re-render. This would cause my smooth transition animation between the routes to break. So I tried solving it in a different way.
<script lang="ts" setup>
import { onMounted, watchEffect, ref } from "vue";
import { useRoute } from "vue-router";
const isLoading = ref<boolean>(true);
const route = useRoute();
const loadData = async (id) => {
isLoading.value = true;
// load dynamic data here and change ref values
isLoading.value = false; // after load
};
watchEffect(() => {
loadData(route.params.id);
});
onMounted(() => {
loadData(route.params.id);
});
</script>

Missing required prop: "slug" - Vue 3

Im build project on forntend vue and backend django. I stuck with problem what i can't resolv.
I trying to send data from ProjectsView.vue to ProjectDetailView.vue. Im trying to use props but i get error:
[Vue warn]: Missing required prop: "slug"
ProjectsView.vue don't send data to ProjectDetailView.vue and axios throw a error
GET http://127.0.0.1:8000/api/v1/projectdetail/undefined/ 500 (Internal Server Error)
I can't find problem in this code.
this is my ProjectsView:
<template>
<div class="container d-flex d-xl-flex justify-content-xl-center">
<div class="d-flex d-sm-flex d-md-flex d-lg-flex d-xl-flex justify-content-center flex-wrap justify-content-sm-center justify-content-md-center justify-content-lg-center justify-content-xl-center">
<div v-for="prof in projects" v-bind:key="prof.id">
<div class="card" style="width: 285px;height: 400px;margin: 5px;border-radius: 15px;">
<div class="card-body text-center">
<img class="img-fluid" :src="prof.avatar" style="width: 150px;border-width: 1px;border-radius: 100px;" />
<h4 class="card-title">
<router-link
:to="{ name: 'projectdetail', params: { slug: prof.slug } }"
>{{ prof.title }}
</router-link>
<fa v-if="prof.is_online" icon="circle" data-bs-toggle="tooltip" title="Online" style="color: rgb(0,197,67);font-size: 12px;padding: 0px;margin-top: 0px;" /></h4>
<h6 class="text-muted card-subtitle mb-2">{{ prof.about }}</h6>
<h6 class="text-muted card-subtitle mb-2">{{ prof.last_online_at }}</h6>
<div v-if="prof.tahs">
<div class="d-inline-block" v-for="tag in prof.tahs.split(',')" v-bind:key="tag">
<span class="badge rounded-pill bg-secondary" style="margin: 1px;">{{tag}}</span>
</div>
</div>
<p class="card-text"></p><a class="card-link" href="#">Link</a><a class="card-link" href="#">Link</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { useToast } from "vue-toastification";
import Project from '#/views/DetailProjectView.vue'
export default {
name: 'Projects',
setup() {
// Get toast interface
const toast = useToast();
return { toast }
},
data() {
return {
projects: [],
errors: [],
}
},
mounted() {
this.getItemsProjects()
},
components: {
Project,
},
methods: {
async getItemsProjects() {
this.$store.commit('setIsLoading', true)
axios
.get('/api/v1/projects/')
.then(response => {
this.projects = response.data
console.log(this.projects)
})
.catch(error => {
console.log(error)
})
this.$store.commit('setIsLoading', false)
},
}
}
</script>
and this is my ProjectDetailView.vue
<template>
<div>working? {{slug}}
</div>
</template>
<script>
import axios from 'axios'
import { useToast } from "vue-toastification";
export default {
name: 'Project',
setup() {
// Get toast interface
const toast = useToast();
return { toast }
},
data() {
return {
project: [],
errors: [],
}
},
mounted() {
this.getItemsProjects()
},
props: {
slug: {
type: String,
required: true,
},
},
methods: {
async getItemsProjects() {
this.$store.commit('setIsLoading', true)
axios
.get(`/api/v1/projectdetail/${this.slug}`)
.then(response => {
this.project = response.data
console.log(this.project)
})
.catch(error => {
console.log(error)
})
this.$store.commit('setIsLoading', false)
},
}
}
</script>
and my router:
import { createRouter, createWebHistory } from 'vue-router'
import store from '../store'
import HomeView from '../views/HomeView.vue'
import Signup from '../views/SignupView.vue'
import Login from '../views/LoginView.vue'
import Dash from '../views/DashView.vue'
import Myacc from '../views/MyAccView.vue'
import Profile from '../views/ProfileView.vue'
import Projects from '../views/ProjectsView.vue'
import ProjectDetail from '../views/DetailProjectView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/signup',
name: 'signup',
component: Signup
},
{
path: '/login',
name: 'login',
component: Login
},
{
path: '/dash',
name: 'dash',
component: Dash,
meta: {
requiredLogin: true
}
},
{
path: '/myacc',
name: 'myacc',
component: Myacc,
meta: {
requiredLogin: true
}
},
{
path: '/profile',
name: 'profile',
component: Profile,
meta: {
requiredLogin: true
}
},
{
path: '/projects',
name: 'projects',
component: Projects,
meta: {
requiredLogin: true
}
},
{
path: '/project/:slug',
name: 'projectdetail',
component: ProjectDetail,
meta: {
requiredLogin: true,
}
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiredLogin) && !store.state.isAuthenticated) {
next('/login')
} else {
next()
}
})
export default router
how to fix this bug? thank you for advice.. im new in vue
edit

module namespace not found in mapGetters() Vue Jest Testing Error

Currently I am writing a jest testing, but running into the following problem which pops up in my terminal. How can I fix that issue here. According to what the community answered on different forums, I added 'namespaced: true' but without any success. So was wondering what I am doing wrong in this case.
import { shallowMount, createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
import Onboarding from '../Onboarding.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Test onboarding', () => {
let getters;
let store;
const mockStore = { dispatch: jest.fn() };
beforeEach(() => {
getters = {
isReturningUser: () => true,
};
// eslint-disable-next-line import/no-named-as-default-member
store = new Vuex.Store({
namespaced: true,
modules: {
requests: {
getters,
mocks: {
$mockStore: mockStore,
},
},
},
});
});
it('check design with snapshot', () => {
const wrapper = shallowMount(Onboarding, {
store,
localVue,
});
expect(wrapper.findAll('[data-test="onboarding-container"]')).toHaveLength(
1,
);
});
});
<template>
<div
v-if="isReturningUser"
class="popup-container"
data-test="onboarding-container"
>
<div class="popup">
<div class="step">
<img :src="activeStep.image" />
<h2>{{ activeStep.title }}</h2>
<p>{{ activeStep.text }}</p>
</div>
<button
v-if="activeStepIndex <= 2"
class="base-button-primary"
#click="nextStep"
>
Volgende
</button>
<button v-else class="base-button-primary" #click="nextStep">
Ik snap het
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'Onboarding',
data() {
return {
activeStepIndex: 0,
steps: [
{
title: 'Bekijk de drukte',
text: 'Bekijk hoe druk het nu is in de stad.',
image: require('#/assets/images/onboarding/step-1.png'),
},
{
title: 'Pas het scherm aan',
text: "Bezoekers, auto's, parkeerplaatsen, hostspots, routes.",
image: require('#/assets/images/onboarding/step-2.png'),
},
],
};
},
computed: {
...mapGetters('onboarding', ['isReturningUser']),
activeStep() {
return this.steps[this.activeStepIndex];
},
iconUrl() {
return require(`~/assets/icons/checkmark.svg`);
},
},
methods: {
nextStep() {
if (this.activeStepIndex < this.steps.length - 1) {
this.activeStepIndex += 1;
} else {
this.$store.commit('onboarding/isReturningUser', true);
this.activeStepIndex = 0;
}
},
},
};
</script>

Vue: Register part of package as component

I have got a wrapper around a package called vue-awesome-swiper, as follows:
Slider.vue
<template>
<div class="media-slider">
<slot :sliderSetting="sliderSettings" :name="name">
<swiper :options="sliderSettings[name]"
class="swiper"
v-if="slides"
ref="default-slider">
<slot name="slides" :slides="slides" :code="code">
<swiper-slide v-for="image in slides" :key="image" v-if="endpoint"
:style="{'background-image': `url('${image}')`}">
</swiper-slide>
</slot>
<div class="swiper-button-next swiper-button-white" slot="button-next"></div>
<div class="swiper-button-prev swiper-button-white" slot="button-prev"></div>
<div class="swiper-pagination" slot="pagination"></div>
<div class="swiper-scrollbar" slot="scrollbar"></div>
</swiper>
</slot>
</div>
</template>
<script>
import { Swiper, SwiperSlide } from 'vue-awesome-swiper';
import 'swiper/css/swiper.css';
import Axios from '../../../../axiosConfig';
// https://github.surmon.me/vue-awesome-swiper/
export default {
components: { Swiper, SwiperSlide },
data: function() {
return {
code: null,
images: [],
defaults: {
'default-slider': {
loop: true,
loopedSlides: 5,
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
},
scrollbar: {
el: '.swiper-scrollbar',
hide: true
}
}
},
sliderSettings: {}
}
},
props: {
endpoint: {
type: String,
default: null
},
settings: {
type: Object,
default: {}
},
theme: {},
name: {},
hash: {},
numberOfImages: {},
imageFormat: {},
vehicleId: {}
},
computed: {
slides() {
if (this.images.length) {
return this.images;
}
return [...Array(parseInt(this.numberOfImages))].map(
(_, i) => {
i++;
return this.imageFormat.replace('#', i);
}
);
}
}
}
</script>
As you can see I have got a slot within this component, however it must use an instance of SwiperSlide for it to work. I need to register this as a component to use it.
However this isn't working as expected:
Vue.component('slider', () => import('./components/Media/Slider/Index'));
Vue.component('slide', () => import('vue-awesome-swiper'));
How can I do this?
Edit
<slider class="app">
<template v-slot:slides="props">
<slide :style="{'background-image': `url('img-src/${props.code}/theme/slide-1.jpg')`}"></slide>
</template>
</slider>
An import like this import() is gonna import the default export of the package, what you have done here is that you have destructured the import with import { .. } from ".." that means it has an named export.
However try it like this
Vue.component('slider', () => import('./components/Media/Slider/Index'));
Vue.component('slide', async () => (await import('vue-awesome-swiper')).SwiperSlide);