How do I load a Vue plugin when loading Vue asyncronously? - vue.js

I'm a bit new to using vue and webpack, and I'm trying to set up a form component. I'm getting an error saying "Uncaught (in promise) TypeError: Vue.use is not a function". The code that I am using is a modification of the code here: https://auralinna.blog/post/2018/how-to-build-a-complete-form-with-vue-js
I am loading Vue into my app asynchronously, so I think the issue may be that Vue isn't loaded yet, but I don't know enough to say for sure.
Here's my main app.js file:
// App main
const main = async () => {
// Import our CSS
//const Styles = await import(/* webpackChunkName: "styles" */ '../css/app.pcss');
// Async load the vue module
const Vue = await import(/* webpackChunkName: "vue" */ 'vue');
const VueI18n = await import(/* webpackChunkName: "vue-i18n" */ 'vue-i18n');
const Vuelidate = await import(/* webpackChunkName: "vuelidate" */ 'vuelidate');
const translations = await import(/* webpackChunkName: "translations" */ '../resources/translations');
const Scrollmagic = await import(/* webpackChunkName: "scrollmagic" */ 'scrollmagic');
Vue.use(VueI18n);
Vue.use(Vuelidate);
const i18n = new VueI18n({
locale: 'en',
fallbackLocale: 'en',
messages: translations
})
// process layout images
// Create our vue instance
const vm = new Vue.default({
el: "#app",
i18n,
delimiters: ['[[', ']]'],
components: {
'carousel': () => import(/* webpackChunkName: "vue-owl-carousel-br" */ '../vue/vue-owl-carousel-br/src/Carousel.vue'),
'appForm': () => import(/* webpackChunkName: "app-form" */ '../vue/form/form.vue'),
},
data: {
menuActive: false,
footerActive: false,
},
methods: {
},
mounted() {
},
});
};
The error message in the console says "Uncaught (in promise) TypeError: Vue.use is not a function" and it is happening at line 14: Vue.use(VueI18n);
I'm trying to resolve the error so that hopefully the form component will render. Can anyone point me to what I need to adjust to resolve this problem?

Related

Load vue component when needed

i have an issue loading a component in vue which should only be there if the route contains a specific string.
In this file there are multiple components as well, which need to be always loaded, only one needs to be loaded when needed.
So far i have this, but doesn't work:
import Vue from 'vue';
import { mapState } from 'vuex';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [
{
path: '/PATH',
name: 'Component3',
component: () =>
import(/* webpackChunkName: "vsf-layout-component3" */ './Component3'),
},
];
const router = new VueRouter({
routes,
});
export default {
router,
components: {
Component1: () => import(/* webpackChunkName: "vsf-layout-component1" */ './Component1'),
Component2: () => import(/* webpackChunkName: "vsf-layout-component2" */ './Component2'),
},
data: () {},
computed: () {},
};
maybe soneone has an idea?
Thanks in regards!

Vue index.html contains <link> tags to several lazy-loaded components

I'm trying to improve the load time of my website, and towards that, I'm using the following settings in my vue.config.js:
config.output.chunkFilename(`js/[name].[chunkhash:8].js`)
config.optimization
.runtimeChunk('single')
.splitChunks({
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name (module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
// npm package names are URL-safe, but some servers don't like # symbols
return `npm.${packageName.replace('#', '')}`
}
}
}
})
Additionally, I also load all my routes dynamically:
router.js
const LandingPage = () => import(/* webpackChunkName: "view-landing-page" */ '#C/landing-page')
const HomePage = () => import(/* webpackChunkName: "view-home-page" */ '#C/home-page')
const Room = () => import(/* webpackChunkName: "view-room" */ '#C/room')
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'landingPage',
component: LandingPage
},
{
path: '/land',
name: 'landingPage',
component: LandingPage
},
{
path: '/home',
name: 'homePage',
component: HomePage
},
{
path: '/*',
name: 'roomPage',
component: Room
}
]
})
Each of these views also use lazy-loaded components. For example:
room.vue
export default {
name: 'room',
mixins: [SocketEventsMixin],
components: {
'video-element': () => import(/* webpackChunkName: "video-element" */ '#RC/media/video-element'),
'video-chat': () => import(/* webpackChunkName: "video-chat" */ '#RC/communications/video-chat'),
}
}
However, even with all this, I see the following in my index.html:
...
<link href=/css/view-room.1ce954c7.css rel=prefetch>
<link href=/js/view-room.fe2de329.js rel=prefetch>
...
<link href=/css/video-element.8342e42f.css rel=prefetch>
<link href=/js/video-element.ab0e6cca.js rel=prefetch>
...
As a result, a whopping 4.5MB is downloaded on visiting the landing page, which does not require any of these assets.
How do I ensure that index.html only contains the bare-minimum scripts/CSS, and have everything else be loaded dynamically? What causes an asset to be included in index.html as opposed to being dynamically loaded? What am I doing wrong?
Vue-CLI automatically sets webpackPrefetch: true for all lazy-loaded components. I do not know how you can conditionally turn it on/off so in my projects I am turning it off completely:
// vue.config.js
chainWebpack: config =>
{
config.plugins.delete('prefetch'); // for async routes
config.plugins.delete('preload'); // for CSS
}

VueJS : Routing confusion

probably a stupid question especially as I am not sure if I am using VueJS or VueJS 2.0 but I am trying to get simple routing working where I can pick up the parameters / the path of the URL.
For example http://127.0.0.1/search/*****
Here is my main.js
import Vue from 'vue'
import App from './components/App'
import VueRouter from 'vue-router'
const routes = [
{ path: '/', name: 'Home', component: App },
{ path: 'search/:id', name: 'Search', component: App }
];
const router = new VueRouter({ mode: 'history', routes: routes });
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
render: h => h(App)
})
And on my App.component I am trying to get the :id
created: function() {
//this.filterTutorials();
this.searchTerm = this.$route.query.id;
if (this.searchTerm == null) {
this.searchTerm = this.$route.params.id;
}
console.log(this.searchTerm)
}
UPDATE
App and search were the same component but I have not split them out (same directory)
New main.js. It is not even calling the search page
import Vue from 'vue'
import App from './components/App'
import VueRouter from 'vue-router'
const routes = [
{ path: '/', name: 'Home', component: App },
{ path: '/search/:id', name: 'Search', component: () => import(/* webpackChunkName: "search" */ './components/Search.vue'), props: true }
];
const router = new VueRouter({ mode: 'history', routes: routes });
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
render: h => h(App)
})
I have also updated webpacks
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
vue: 'vue/dist/vue.js'
}
},
In your case, App is statically created before the route even resolves, so the created lifecycle hook would check for route parameter before it even existed (i.e., it would be undefined). I noticed both /search and / point to App, but you probably meant a component name like Search.
You can either dynamically import Search:
const routes = [
{
path: '/search/:id',
name: 'Search',
component: () => import(/* webpackChunkName: "search" */ './views/Search.vue')
}
]
Or you could use VueRouter's props: true to automatically set Search's id prop on navigation, obviating the check for the route parameters from created().
const routes = [
{
path: '/search/:id',
name: 'Search',
component: () => import(/* webpackChunkName: "search" */ './views/Search.vue'),
props: true,
}
]
demo

Router beforeEach guard executed before state loaded in Vue created()

If I navigate directly to an admin guarded route, http://127.0.0.1:8000/dashboard/, the navigation is always rejected because the state hasn't yet loaded at the time the router guard is checked.
beforeEach is being executed before Vue created and thus the currently logged in user isn't recognized.
How do I get around this chicken and egg issue?
files below truncated for relevancy
main.js
router.beforeEach((to, from, next) => {
//
// This is executed before the Vue created() method, and thus store getter always fails initially for admin guarded routes
//
// The following getter checks if the state's current role is allowed
const allowed = store.getters[`acl/${to.meta.guard}`]
if (!allowed) {
return next(to.meta.fail)
}
next()
})
const app = new Vue({
router,
store,
el: "#app",
created() {
// state loaded from localStorage if available
this.$store.dispatch("auth/load")
},
render: h => h(App)
})
router.js
export default new VueRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: () => import('../components/Home.vue'),
meta: {
guard: "isAny",
},
},
{
path: '/dashboard/',
name: 'dashboard',
component: () => import('../components/Dashboard.vue'),
meta: {
guard: "isAdmin",
},
},
],
})
Take this.$store.dispatch("auth/load") out of the Vue creation and run it before the Vue is created.
store.dispatch("auth/load")
router.beforeEach((to, from, next) => {...}
new Vue({...})
If auth/load is asynchronous, then return a promise from it and do your code initialize your Vue in the callback.
store.dispatch("auth/load").then(() => {
router.beforeEach((to, from, next) => {...}
new Vue({...})
})

How to write test that mocks the $route object in vue components

I have a component that contains statement like this.$route.fullPath, how should I mock value of fullPathof $route object if I want to test that component?
I disagree with the top answer - you can mock $route without any issue.
On the other hand, installing vue-router multiple times on the base constructor will cause you problems. It adds $route and $router as read only properties. Which makes it impossible to overwrite them in future tests.
There are two ways to achieve this with vue-test-utils.
Mocking vue-router with the mocks option
const $route = {
fullPath: 'full/path'
}
const wrapper = mount(ComponentWithRouter, {
mocks: {
$route
}
})
wrapper.vm.$route.fullPath // 'full/path'
You can also install Vue Router safely by using createLocalVue:
Installing vue-router safely in tests with createLocalVue
const localVue = createLocalVue()
localVue.use(VueRouter)
const routes = [
{
path: '/',
component: Component
}
]
const router = new VueRouter({
routes
})
const wrapper = mount(ComponentWithRouter, { localVue, router })
expect(wrapper.vm.$route).to.be.an('object')
Best not mock vue-router but rather use it to render the component, that way you get a proper working router. Example:
import Vue from 'vue'
import VueRouter from 'vue-router'
import totest from 'src/components/totest'
describe('totest.vue', () => {
it('should totest renders stuff', done => {
Vue.use(VueRouter)
const router = new VueRouter({routes: [
{path: '/totest/:id', name: 'totest', component: totest},
{path: '/wherever', name: 'another_component', component: {render: h => '-'}},
]})
const vm = new Vue({
el: document.createElement('div'),
router: router,
render: h => h('router-view')
})
router.push({name: 'totest', params: {id: 123}})
Vue.nextTick(() => {
console.log('html:', vm.$el)
expect(vm.$el.querySelector('h2').textContent).to.equal('Fred Bloggs')
done()
})
})
})
Things to note:
I'm using the runtime-only version of vue, hence render: h => h('router-view').
I'm only testing the totest component, but others might be required if they're referenced by totest eg. another_component in this example.
You need nextTick for the HTML to have rendered before you can look at it/test it.
One of the problems is that most of the examples I found referred to the old version of vue-router, see the migrations docs, eg. some examples use router.go() which now doesn't work.
No answer was helping me out, So I dig into vue-test-utils documentation and found myself a working answer, so you need to import.
import { shallowMount,createLocalVue } from '#vue/test-utils';
import router from '#/router.ts';
const localVue = createLocalVue();
We created a sample vue instance. While testing you need to use shallowMount so you can provide vue app instance and router.
describe('Components', () => {
it('renders a comment form', () => {
const COMMENTFORM = shallowMount(CommentForm,{
localVue,
router
});
})
})
You can easily pass router and to shallow mount and it does not gives you the error. If you want to pass store you use:
import { shallowMount,createLocalVue } from '#vue/test-utils';
import router from '#/router.ts';
import store from '#/store.ts';
const localVue = createLocalVue();
And then pass store:
describe('Components', () => {
it('renders a comment form', () => {
const COMMENTFORM = shallowMount(CommentForm,{
localVue,
router,
store
});
})
})
This solution solved the following errors:
Cannot read property 'params' of undefined when using this.$route.params.id
Unknown custom element router-link
✔
Easiest method i found is to use localVue
import { createLocalVue, mount } from '#vue/test-utils';
import VueRouter from 'vue-router';
import Vuex from 'vuex';
import ComponentName from '#/components/ComponentName.vue';
// Add store file if any getters is accessed
import store from '#/store/store';
describe('File name', () => {
const localVue = createLocalVue();
localVue.use(VueRouter);
// Can also be replaced with route(router.js) file
const routes = [
{
path: '/path',
component: ComponentName,
name: 'Route name'
}
];
const router = new VueRouter({ routes });
// if needed
router.push({
name: 'Route name',
params: {}
});
const wrapper = mount(ComponentName, {
localVue,
router,
store
});
test('Method()', () => {
wrapper.vm.methodName();
expect(wrapper.vm.$route.path)
.toEqual(routes[0].path);
});
});
Hope it helps!!!
Why are all answers so complicated? You can just do:
...
wrapper = mount(HappyComponent, {
mocks: {
$route: { fullPath: '' }
},
})
...
You dont have to specifically "mock" a router. Your application can set VueRouter in the global vue scope and you can still make it do what you want in your tests without issue.
Read the localVue usage with VueRouter: https://vue-test-utils.vuejs.org/guides/#using-with-vue-router.
I am currently pulling in a complex router from our main app and am able to jest.spyOn() calls to router.push() as well as setting the path before the component is created running shallowMount() for some route handling in a created() hook.
The Workaround
// someVueComponent.vue
<template>
... something
</template>
<script>
...
data () {
return {
authenticated: false
}
},
...
created () {
if(!this.authenticated && this.$route.path !== '/'){
this.$router.push('/')
}
}
</script>
// someVueComponent.spec.js
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import { shallowMount, createLocalVue } from '#vue/test-utils'
import SomeVueComponent from 'MyApp/components/someVueComponent'
import MyAppRouter from 'MyApp/router'
import MyAppCreateStore from 'MyApp/createStore'
import merge from 'lodash.merge'
function setVueUseValues (localVue) {
localVue.use(Vuex)
localVue.use(VueRouter)
// other things here like custom directives, etc
}
beforeEach(() => {
// reset your localVue reference before each test if you need something reset like a custom directive, etc
localVue = createLocalVue()
setVueUseValues(localVue)
})
let localVue = createLocalVue()
setVueUseValues(localVue)
test('my app does not react to path because its default is "/"', () => {
const options = {
localVue,
router: MyAppRouter,
store: MyAppCreateStore()
}
const routerPushSpy = jest.spyOn(options.router, 'push')
const wrapper = shallowMount(SomeVueComponent, options)
expect(routerPushSpy).toHaveBeenCalledTimes(0)
})
test('my app reacts to path because its not "/" and were not authenticated', () => {
const options = {
localVue,
router: MyAppRouter,
store: MyAppCreateStore()
}
const routerPushSpy = jest.spyOn(options.router, 'push')
options.router.push('/nothomepath')
expect(routerPushSpy).toHaveBeenCalledWith('/nothomepath') // <- SomeVueComponent created hook will have $route === '/nothomepath' as well as fullPath
const wrapper = shallowMount(SomeVueComponent, options)
expect(routerPushSpy).toHaveBeenCalledWith('/') // <- works
})
The above is done with the idea that I need the $route state changed before SomeVueComponent.vue is created/mounted. Assuming you can create the wrapper and want to test that the component this.$router.push('/something') based on some other state or action you can always spy on the wrapper.vm instance
let routerPushSpy = jest.spyOn(wrapper.vm.$router, 'push') // or before hooks, etc
As of this writing there seems to be an open defect which keeps the following from working because vm.$route will always be undefined, making the above the only option (that I know of) as there is no other way to "mock" the $route because installing VueRouter writes read only properties to $route.
From the vue-test-utils docs https://vue-test-utils.vuejs.org/guides/#mocking-route-and-router:
import { shallowMount } from '#vue/test-utils'
const $route = {
path: '/some/path'
}
const wrapper = shallowMount(Component, {
mocks: {
$route
}
})
wrapper.vm.$route.path // /some/path
If your interested here is the github link to a reproduction of the issue: https://github.com/vuejs/vue-test-utils/issues/1136
All kudos to #SColvin for his answer; helped find an answer in my scenario wherein I had a component with a router-link that was throwing a
ERROR: '[Vue warn]: Error in render function: (found in <RouterLink>)'
during unit test because Vue hadn't been supplied with a router. Using #SColvin answer to rewrite the test originally supplied by vue-cli from
describe('Hello.vue', () =>
{
it('should render correct contents', () =>
{
const Constructor = Vue.extend(Hello);
const vm = new Constructor().$mount();
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
to
describe('Hello.vue', () =>
{
it('should render correct contents', () =>
{
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{ path: '/', name: 'Hello', component: Hello },
],
});
const vm = new Vue({
el: document.createElement('div'),
/* eslint-disable object-shorthand */
router: router,
render: h => h('router-view'),
});
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
});
Not needing to pass parameters in to the view I could simplify the component as the default render, no need to push and no need to wait nextTick. HTH someone else!
Adding to the great answer from #SColvin, here's an example of this working using Avoriaz:
import { mount } from 'avoriaz'
import Vue from 'vue'
import VueRouter from 'vue-router'
import router from '#/router'
import HappyComponent from '#/components/HappyComponent'
Vue.use(VueRouter)
describe('HappyComponent.vue', () => {
it('renders router links', () => {
wrapper = mount(HappyComponent, {router})
// Write your test
})
})
I believe this should work with vue-test-utils, too.
Take a look at this example using vue-test-utils, where I'm mocking both router and store.
import ArticleDetails from '#/components/ArticleDetails'
import { mount } from 'vue-test-utils'
import router from '#/router'
describe('ArticleDetails.vue', () => {
it('should display post details', () => {
const POST_MESSAGE = 'Header of our content!'
const EXAMPLE_POST = {
title: 'Title',
date: '6 May 2016',
content: `# ${POST_MESSAGE}`
}
const wrapper = mount(ArticleDetails, {
router,
mocks: {
$store: {
getters: {
getPostById () {
return EXAMPLE_POST
}
}
}
}
})
expect(wrapper.vm.$el.querySelector('h1.post-title').textContent.trim()).to.equal(EXAMPLE_POST.title)
expect(wrapper.vm.$el.querySelector('time').textContent.trim()).to.equal(EXAMPLE_POST.date)
expect(wrapper.vm.$el.querySelector('.post-content').innerHTML.trim()).to.equal(
`<h1>${POST_MESSAGE}</h1>`
)
})
})
This is what I've been doing as per this article:
it('renders $router.name', () => {
const scopedVue = Vue.extend();
const mockRoute = {
name: 'abc'
};
scopedVue.prototype.$route = mockRoute;
const Constructor = scopedVue.extend(Component);
const vm = new Constructor().$mount();
expect(vm.$el.textContent).to.equal('abc');
});
You can mock to vm.$router by setting vm._routerRoot._router
For example
var Constructor = Vue.extend(Your_Component)
var vm = new Constructor().$mount()
var your_mock_router = {hello:'there'}
vm.$router = your_mock_router //An error 'setting a property that has only a getter'
vm._routerRoot._router = your_mock_router //Wow, it works!
You can double check their source code here: https://github.com/vuejs/vue-router/blob/dev/dist/vue-router.js#L558
Easiest way i've found is to mock the $route.
it('renders $router.name', () => {
const $route = {
name: 'test name - avoriaz'
}
const wrapper = shallow(Component, {
mocks: {
$route
}
})
expect(wrapper.text()).to.equal($route.name)
})