(vue-test-utils) 'could not overwrite property $route,...' - javascript

Hi I'm testing vue project using vue-test-utils.
What I wanna do is to test router (my project is using VueRouter).
I know that if I import routes from router file or VueRouter and use it to localVue, $route and $router properties are read-only so I can't mock it.
But when I tried like
transfers.test.js
import { createLocalVue, shallowMount } from '#vue/test-utils'
// import VueRouter from 'vue-router'
// import routes from '../../router/Routes'
import ElementUI from 'element-ui'
...
const localVue = createLocalVue()
// localVue.use(VueRouter)
localVue.use(ElementUI)
localVue.use(Vuex)
localVue.component('DefaultLayout', DefaultLayout)
// const router = new VueRouter({ routes })
...
describe('Elements', () => {
const div = document.createElement('div')
div.id = 'root'
document.body.appendChild(div)
const wrapper = shallowMount(Transfers, {
localVue,
store,
mocks: {
$route: {
params: { processing: 'failed' }
}
},
// router,
attachTo: '#root'
})
afterAll(() => {
wrapper.destroy()
})
...
console.error
[vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property as a read-only value
appears.
Actually, what I truly wanna do is not to mock route but to use real router but there is another issue..
'Route with name 'something' does not exist' vue-router console.warn when using vue-test-utils
If you know the solutions for these issues, please let me know! Thank you for in advance.

I certainly solved this problem with the first answer of this question!
vue-test-utils: could not overwrite property $route, this is usually caused by a plugin that has added the property as a read-only value
It seems like using VueRouter in anywhere(even in non-test codes) affects mocking $route.
if (!process || process.env.NODE_ENV !== 'test') {
Vue.use(VueRouter)
}
I solved with this code in the first answer of question linked. Use this code to where VueRouter is used!

Related

Why does export const work but export function does not?

I am trying to ensure that the same instance of vue-router is available in multiple places in my app.
The vue.router.js file exports the router like this:
export function createVueRouter() {
return createRouter({
history: createWebHistory('/')
routes: [//array of routes]
});
}
In my main app.js file (or main.js if you prefer) it is used like this:
import {createVueRouter} from './router/vue/vue.router.js';
const router = createVueRouter();
app.use(router);
The Vue app now has access to the vue-router. I now want the same vue-router to be available in an independent JS file. But the following does not work:
resusable.js
import {createVueRouter} from './router/vue/vue.router.js';
const router = createVueRouter();
//....
if (isLoggedInBasic !== true) {
return await router.push({name: "Signin"})
}
It creates a new instance of the vue-router and the router.push does not take the user to the Signin route (but the URL in the address bar does change).
If I change vue.router.js export to the following, it works fine all the files with the same router instance being shared between them.
export const VueRouter = createRouter({
history: createWebHistory('/')
routes: [//array of routes]
})
I cannot understand what is the difference and why it works when export const is used rather than just export function ()

How to access a vue root property that have been loaded in created life hook?

My app is a Vue 2 front end app that load a configuration file since it is a generic front end and each client has its own configuration file.
The configuration is a Yaml file that is loaded when the application starts.
The configuration file has properties like:
urlBtn: https://www.youtube.com/
This is my main.js:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import axios from "axios";
import Vuelidate from "vuelidate";
import yaml from "js-yaml";
import fs from "fs";
import { BootstrapVue } from "bootstrap-vue";
import {
BIcon,
BIconChevronDoubleDown,
BIconChevronDoubleUp,
BIconEyeFill,
BIconEyeSlashFill
} from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
Vue.use(Vuelidate);
Vue.use(BootstrapVue);
Vue.component("BIcon", BIcon);
Vue.component("BIconChevronDoubleDown", BIconChevronDoubleDown);
Vue.component("BIconChevronDoubleUp", BIconChevronDoubleUp);
Vue.component("BIconEyeFill", BIconEyeFill);
Vue.component("BIconEyeSlashFill", BIconEyeSlashFill);
Vue.prototype.$axios = axios;
Vue.config.productionTip = false;
export const app = new Vue({
data() {
return {
ebaConfig: null,
publicPath: process.env.BASE_URL
};
},
methods: {
async loadEbaConfig() {
const config = await axios.get(`${this.publicPath}config.yaml`);
const doc = yaml.load(config.data);
this.ebaConfig = doc;
}
},
router,
store,
created() {
this.loadEbaConfig();
},
render: h => h(App)
}).$mount("#app");
When i try to access the ebaConfig property in a the mounted life hook of another component, it's null.
Example:
mounted() {
if (this.$root.ebaConfig.urlBtn.trim()) {
this.showCecomaWebBtn = true;
}
}
In the example above, I get the error:
[Vue warn]: Error in mounted hook: "TypeError: Cannot read properties of null (reading 'urlBtn')"
found in
---> <App> at src/App.vue
<root>
I don't understand why it happens since the property load is executed in the created lifecycle. Therefore it should be available to the other components in the mounted lifecycle. How can I solve that? What am I doing wrong?
Note:
The config file is a valid yaml file since everything works correctly when I use that property in a normal way, that is, outside of the life cycles.
It seems that, despite being in the created life cycle, the data is loaded at the end of the complete loading of the application.

[Vue warn]: Failed to resolve component when tried to create global component

I new on Vue Typescript. I've tried to create global components but I got a warning and component not loaded on the template. This how I tried to create global components
App.vue
import { createApp } from "vue"
import App from "./App.vue"
import "./registerServiceWorker"
import "./globalComponents"
import router from "./router"
import store from "./store"
createApp(App)
.use(store)
.use(router)
.mount("#app")
globalComponents.ts
import { createApp } from "vue"
const app = createApp({})
// Forms
app.component("ui-input", () => import("#/components/core/ui/Input.vue"))
Input.vue
<template lang="pug">
.ui-input
input(v-model="$attrs" v-on="$listeners")
</template>
<script lang="ts">
import { defineComponent } from "vue"
export default defineComponent({
inheritAttrs: false
})
</script>
Hope you all can help me, Thanks in advance
As of Vue 3, if you create an app using createApp, it will be a standalone Vue App instance. So if you need to add a global component then you will need to add it on the app object created from createApp, here's the code for that:
const app = createApp({});
app.component('my-component-name', MyComponent) // <-- here you can register.
app.mount("#app");
But if there are a lot of components then adding them in the main.ts file will be a mess, so we can create another file, like you did, so:
Your current globalComponents.ts
import { createApp } from "vue"
const app = createApp({})
// Forms
app.component("ui-input", () => import("#/components/core/ui/Input.vue"))
The Problem
But notice here's a mistake. What is it? You created another app using createApp. As I referred earlier that if you need to create a global component, you can only create on the same instance.
Fix
As we know the problem here is that we are creating another instance, which is again a new and standalone instance, so we need to figure out the way we can have the same app instance in globalComponents.ts as well, so we will pass the app from main-ts to globalComponents.ts, like:
globalComponents.ts
import { App } from "vue";
// register components
export const registerComponents = (app: App): void => {
app.component("ui-input", () => import("#/components/core/ui/Input.vue"));
}
And now you can call registerComponents in main.ts as:
main.ts
const app = createApp(App)
.use(store)
.use(router);
registerComponents(app); // <-- here you go
app.mount("#app");
You will still get something like:
[Vue warn]: Invalid VNode type: undefined (undefined).
You can read more here about how to define an async component in vue 3. To fix that error you will need to wrap your import in defineAsyncComponent as:
globalComponents.ts
import { defineAsyncComponent } from "vue";
// register components
export const registerComponents = (app) => {
app.component(
"ui-input",
defineAsyncComponent(() => import("#/components/Input.vue"))
);
};

how to import components to add them to routes with Vue and VueRouter

in my project I have a folder called views, for my views in a spa, what I try is to avoid the manual import of each element, is this possible?
to declare components I investigated this method.
const files = require.context('./', true, /\.vue$/i)
files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0],
files(key).default))
how to do this if the syntax is
import Home from '../views/Home ';
I tried this, because this is the idea but it obviously marks me error, what would be the best solution?
const files = require.context('../views/', true, /\.vue$/i)
files.keys().map(key => {
import key.split('/').pop().split('.')[0] from '../views/'+ files(key);
})
/* Routers */
export default [
/* Rutas de Venta venta */
{
path: '/',
name: 'home',
component: Home,
},
Chris Fritz had a talk where explain it
His presentation is in github
I don't understand so much this code, but it works
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
// Require in a base component context
const requireComponent = require.context(
'./components', false, /base-[\w-]+\.vue$/
);
requireComponent.keys().forEach(fileName => {
// Get component config
const componentConfig = requireComponent(fileName)
// Get PascalCase name of component
const componentName = upperFirst(
camelCase(filename.replace(/^\.\//,'').replace(/\.\w+$/, ''))
)
// Register component globally
Vue.component(componentName, componentConfig.default || componentConfig)
})

How to navigate using vue router from Vuex actions

I am creating a web app with Vue 2.x and Vuex 2.x. I am fetching some information from a remote location via an http call, I want that if that call fails I should redirect to some other page.
GET_PETS: (state) => {
return $http.get('pets/').then((response)=>{
state.commit('SET_PETS', response.data)
})
},
error => {this.$router.push({path:"/"}) }
)
}
But this.$router.push({path:"/"}) gives me following error.
Uncaught (in promise) TypeError: Cannot read property 'push' of undefined
How can this be achieved.
Simulated JsFiddle: here
import router from './router'
and use router.push
Simple like that.
This example may help you.
main.js
import Vue from "vue";
import VueRouter from "vue-router";
...
Vue.use(VueRouter);
export const router = new VueRouter({
mode: 'hash',
base: "./",
routes: [
{ path: "/", component: welcome},
{ path: "/welcome", component: welcome},
]
})
actions.js
import {router} from "../main.js"
export const someAction = ({commit}) => {
router.push("/welcome");
}
It looks like you aren't injecting your router into your app, hence it being 'undefined'
In previous versions of vue-router you would: Vue.use(VueRouter), with 2.0 you can inject the router into the app like below:
const routes = [
{ path: '/foo', component: Foo },
]
const router = new VueRouter({
routes
})
const app = new Vue({
router // inject the router
}).$mount('#app')
this should then make it available as this.$router throughout the app
Following answering a related question: How to use Vue Router from Vuex state? it seems that Vuex won't receive the router instance at this.$router. Therefore two methods were suggested to provide access to the router instance.
The first is more direct which involves setting a webpack global to the instance.
The second involves using Promises with your vuex action that would allow your components to utilise their reference to the router instance following the actions Promise resolving / rejecting.
INITIAL ANSWER
In main.js (the one, where we "install" all modules and create Vue instance, i.e. src/main.js):
const vm = new Vue({
el: '#app',
router,
store,
apolloProvider,
components: { App },
template: '<App/>'
})
export { vm }
This is my example, but in our case the most important here is const vm and router
In your store:
import { vm } from '#/main'
yourMutation (state, someRouteName) {
vm.$router.push({name: someRouteName})
}
P.S. Using import { vm } from '#/main' we can access anything we need in Vuex, for example vm.$root which is needed by some components of bootstrap-vue.
P.P.S. It seems we can use vm just when everything is loaded. In other words we can not use vm inside someMutation in case, if we call someMutation inside mounted(), because mounted() comes/occurs before vm is created.
NEW ANSWER
Constantin's answer (the accepted one) is better than mine, so just want to show for novice how to implement it.
Inside core dir (inside /src in my case), next to App.vue, main.js and others I have router.js with the content:
import Vue from 'vue'
import Router from 'vue-router'
// Traditional loading
import Home from '#/components/pages/Home/TheHome'
// Lazy loading (lazy-loaded when the route is visited)
const Page404 = () => import(/* webpackChunkName: "Page404" */ '#/components/pages/404)
const Page503 = () => import(/* webpackChunkName: "Page503" */ '#/components/pages/503)
Vue.use(Router)
const router = new Router({
mode: 'hash',
base: process.env.BASE_URL,
linkExactActiveClass: 'active',
routes: [
{
path: '*',
name: 'Page404',
component: Page404
},
{
path: '*',
name: 'Page503',
component: Page503
},
{
path: '/',
name: 'Home',
component: Home
},
// Other routes
{....},
{....}
]
})
// Global place, if you need do anything before you enter to a new route.
router.beforeEach(async (to, from, next) => {
next()
})
export default router
Import our router to main.js:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
const vm = new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
export { vm }
Finally, inside your component, or Vuex or anywhere else import router from './router' and do whatever you need, such as router.push(...)
I didn't like keeping my app's location state separate from the rest of my app state in the Store, and having to manage both a Router and a Store, so I created a Vuex module that manages the location state inside the Store.
Now I can navigate by dispatching actions, just like any other state change:
dispatch("router/push", {path: "/error"})
This has the added benefit of making things like animated page transitions easier to handle.
It's not hard to roll your own router module, but you can also try mine if you want to:
https://github.com/geekytime/vuex-router
You can simply import route from router directory like this:
import router from '#/router'
router.push({name: 'Home'})
this # symbol replaces the path to the src directory

Categories