vuejs : how to unit test main.ts - javascript

I'm using vuejs with typescript, but the question would apply with javascript too.
With vue-test-utils, components are tested using mount or shallowMount. This way, I've been able to unit test the App main component. I'm now wondering how I can unit test the main.ts file, which does already mount the main component:
new Vue({
router,
store,
i18n,
render: (h) => h(App),
}).$mount('#app');
The unit test would check if App component is really mounted into #app.
Unsurprisingly, if I just import main.ts in my test, I get this error:
Cannot find element: #app
Is it possible to do something to create a fake DOM containing an #app element, in which the App component would be mounted?

You might use Jest to mock a DOM. This way you could setup multiple tests:
initializing: *Verify that without any parameters it can still instantiate the Vue instance (undefined as router, store, etc). Verify that the properties like router etc are added to the prototype
Mounting: Create a mock App which renders to your created DOM and verify that the app renders as expected to the correct element
Especially mounting feels more like functional testing instead of unit testing (as it performs more than a 'unit' of work)
EDIT: provide example
// __tests__/main.test.js
'use strict';
test('Can mount app', () => {
document.body.innerHTML =
'<div id="app">' +
'</div>';
// Executes main file
require('../src/main');
const pElement = document.getElementById('example');
expect(pElement).toBeTruthy();
expect(pElement.textContent).toEqual('Example');
});
With a main file of:
// main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');
And a App.vue file of:
<template>
<div id="app">
<p id="example">Example</p>
</div>
</template>
Rewriting to Typescript is trivial. A few points to notice:
The App itself is not mocked (Vue.app as injected in main.js). This can be done by creating a separate mock file or using jest.fn() although I have not tested this myself.
The tests are not functional testing it but with small changes can be made to verify small units of work

Related

Vue component inside legacy php/jquery project

I've got this big legacy web app based on Codeigniter and jQuery.
The plan is to phase out jQuery and start using Vuejs instead. We are replacing certain parts of the website step by step.
I have just installed Nuxtjs and got the file structure the way i like it inside the legacy project.
Now to the question. How can i access a Vue component from one of my legacy javascript files?
App.vue
<template>
<div id="app">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import HelloWorld from "./components/HelloWorld.vue";
export default Vue.extend({
name: "App",
components: {
HelloWorld
}
});
</script>
main.ts
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
Vue.config.productionTip = false;
new Vue({
store,
render: h => h(App)
}).$mount("#app");
I want to display my App.vue inside an ordinary php/html view.
I am doing something similar right now, the project was originally written using Thymeleaf and jQuery and now we are switching to Vue.
You can communicate between vue components and non-vue components in a few ways, none of them are "pretty".
Communication
Classic JavaScript events
Pretty straightforward
// legacy code
document.dispatchEvent(new CustomEvent('legacy-vue.something-happened', { detail: payload }))
// vue component
created () {
document.addEventListener('legacy-vue.something-happened', this.onSomethingHappened)
},
destroyed () { // don't forget to remove the listener!
document.removeEventListener('legacy-vue.something-happened', this.onSomethingHappened)
}
Exposing EventHub
Similar to the previous one, but you are using vue events instead. This is what i would recommend, because it's the Vue way of handling events and your goal is to vuetify your app.
// initialization
const hub = new Vue()
Vue.prototype.$eventHub = hub
window.$vueEventHub = hub
// legacy code
$vueEventHub.$emit('something-happened', payload)
// vue component
created () {
this.$eventHub.$on('something-happened', this.onSomethingHappened)
},
destroyed () {
this.$eventHub.$off('something-happened', this.onSomethingHappened)
}
Exposing whole components
The most flexible way, but it's hard to see what's going on where. Event based approaches the lesser evil in my opinion (it's easy to track events).
// vue component
created () {
window.vueTableComponent = this
}
// legacy component
vueTableComponent.fetchNextPage()
vueTableComponent.registerOnPageFetchedCallback(callback);
Summary
No matter which approach you pick, i recommend doing something like this:
Let's say that you have TableComponent. TableComponent has few props like apiUrl, emits row-clicked event, etc. It would be best to design the component without thinking about your legacy code at all, and then create it's legacy wrapper because at one point it will be used both with vue-only screens and with mixed-screens (with both legacy components and vue components). An example:
LegacyTableComponentWrapper.vue
<template>
<table-component
:api-path="apiPath"
#row-clicked="onRowClicked"
ref="table-component"
/>
</template>
export default {
data: () => ({
apiPath: null
}),
methods: {
onRowClicked (row) {
this.$eventHub.$emit('table-row-clicked', row) // notify legacy code
},
onApiPathDefined (payload) {
this.apiPath = payload
}
},
mounted () {
// legacy code might require the TableComponent to act differently
// and if you don't want the TableComponent to know whenever it's legacy or not
// you can always override one or more of it's methods.
this.$refs['table-component'] = this.overriddenImplementationOfXYZ
},
created () {
this.$eventHub.$on('define-api-path', this.onApiPathDefined)
},
destroyed () {
this.$eventHub.$off('define-api-path', this.onApiPathDefined)
}
}
It's sure more work at first, but will save you a headache later, when you will be working on your first view which is fully in vue and all that legacy communication stuff is in the way.

Manually mount a globally registered Vue component into a legacy app?

I am converting an AngularJS project to Vue 2.
My strategy is to have AngularJS handle routing, and slowly swap out individual components within each angular view.
In my main.ts file, I'm importing and defining global components like this:
import LoginForm from './components/LoginForm.vue';
Vue.component('LoginForm', LoginForm);
When the relevant Angular controller loads, I'd like to run some javascript to manually mount the Vue component.
Is this a good approach?
How can I manually mount a previously registered component?
Vue.somehowGetComponent('LoginForm').mountTo('#login-form');
I wouldn't register it as a global component for this purpose. Simply import it, then create an instance of it and mount it. eg.
// import the component
import LoginForm from './components/LoginForm.vue';
// If you haven't already, must call `Vue.extend` on LoginForm before its instantiation
const myLoginForm = new LoginForm();
// mount to your legacy app
myLoginForm.$mountTo(document.getElementById('login-form'));
The key here was calling Vue.extend on my imported component:
In Vue.js:
// main.ts
import LoginForm from './components/LoginForm.vue';
window.LoginForm = Vue.extend(LoginForm)
Then in the AngularJS controller:
angular.module('app').controller(function(){
var vue = new window.LoginForm();
vue.$mount('#login-form');
});

Vue/Vuetify - Unknown custom element: <v-app> - did you register the component correctly?

I am new to Vue and Vuetify. I just created quick app to check both of them. But I am a running into issues in beginning. The vue fails to identify vuetify components despite following all the steps outlined in document. The error is like below -
vue.runtime.esm.js?ff9b:587 [Vue warn]: Unknown custom element:
- did you register the component correctly? For recursive
components, make sure to provide the "name" option.
found in
---> at src\App.vue
You can access the entire code at sandbox https://codesandbox.io/s/40rqnl8kw
You're likely experiencing a problem with the order of your operations. You're defining your own App component that uses the v-app component before you've even told Vue to make use of it, so Vue assumes you're using your own custom v-app component.
Place Vue.use(Vuetify) before starting any Vue instances via new Vue() that require Vuetify components, or place it within the component definitions themselves right at the top of the <script> tag after importing Vue and Vuetify within the single file component. Don't worry if you have more than one Vue.use(Vuetify) statement because only the first one will do anything--all subsequent calls will simply do nothing.
Original - Vue.use() is called before new Vue(), resulting in an error.
new Vue({
el: "#app",
components: { App },
template: "<App/>"
});
Vue.use(Vuetify);
Fix - Calling new Vue() after Vue.use() allows Vue to resolve the dependency correctly.
Vue.use(Vuetify);
new Vue({
el: "#app",
components: { App },
template: "<App/>"
});
There is another reason for this error that I recently ran into.
I recently upgraded from Vuetify 1.5 to 2.x and even though I had the order of operations correct as in the currently accepted answer here I was still receiving the error about v-app being unknown:
Unknown custom element: <v-app> - did you register the component
correctly? For recursive components, make sure to provide the "name"
option.
Turns out that the upgrade process requires the following addition to package.json devDependencies section which didn't originally exist in my vuetify 1.5x package:
"vuetify-loader": "^1.3.0"
(1.3.0 current version as of this writing)
Once I added that the error went away.
If you are coming from Google: for me it was breaking changes from v1 to v2, that made most Codepen examples useless.
I had to change this to get a very simple Vuetify app with navigation drawers to run again:
remove toolbar from <v-app toolbar>
replace v-toolbar with v-app-bar
replace v-app-bar-side-icon with v-app-bar-nav-icon
replace v-app-bar-title with v-toolbar
replace v-list-tile to v-list-item
replace all flat with text
Maybe this helps someone.
(edited to include cong yu's remark)
Edit: it looks like VuetifyLoader will automatcially do that for you.
Old Answer: Another possible problem is if you have a la carte enabled you will need to also specify all the components that you want included:
import Vue from 'vue'
import App from './App.vue'
import Vuetify, {
VApp, // required
VNavigationDrawer,
VFooter,
VToolbar,
VFadeTransition
} from 'vuetify/lib'
import { Ripple } from 'vuetify/lib/directives'
Vue.use(Vuetify, {
components: {
VApp,
VNavigationDrawer,
VFooter,
VToolbar,
VFadeTransition
},
directives: {
Ripple
}
})
With vuetify v2.x you should register the vuetify plugin as follows :
import Vue from 'vue'
/** register vuetify plugin globally **/
import Vuetify from 'vuetify/lib'
Vue.use(Vuetify)
const opts = {}
const vuetify= new Vuetify(opts)
/****/
new Vue({
vuetify,
}).$mount('#app')
Vuetify v3
import { createApp } from 'vue'
import App from './App.vue'
/*****/
import '#mdi/font/css/materialdesignicons.css'
import 'vuetify/lib/styles/main.sass'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
const vuetify= createVuetify({
components,
directives,
})
/****/
const app = createApp(App)
app.use(vuetify)
app.mount('#app')
I experienced the same issue. It was caused by the cache of the browser make sure to clear the cache as well.
You will get this error even after installing the official Vuetify 3 (Alpha), due to the standard demo version generated during the install lacking adding components, i.e:
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
const vuetify = createVuetify({
components,
directives,
});
Thus the working version of main.ts for Vuetify 3 is:
import "vuetify/styles"; // Global CSS has to be imported
import { createApp } from "vue";
import { createVuetify } from "vuetify";
import App from "./App.vue";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
const app = createApp(App);
const vuetify = createVuetify({
components,
directives,
});
app.use(vuetify).mount("#app");
// or app.use(vuetify); app.mount("#app");
In case someone like me new working on vue and nuxt. My mistake was that I did not put the s in the last. buildModule should be buildModules.
My nuxt.config.js:
export default {
buildModules:[
"#nuxtjs/vuetify"
],
module:[
"#nuxtjs/axios"
],
components:true
}

How can I insert a VueJS app into an existing page?

At this project I'm working on there is a legacy server-rendered web page and some components had problems I've been assigned to fix, and I convinced the team to rewrite those parts in Vue to kickstart our migration.
I wrote the whole mini-app using the Webpack template provided by Vue CLI and it works like a charm... in that specific environment.
If I npm run build the built index.html also works fine in a static server.
However, I can't seem to include the app in an existing page composed of many other elements. Shouldn't it be as simple as adding the <div id='myApp'></div> element to the HTML and loading the generated JS files?
If it helps, the legacy app is a Rails app using .erb templates and the JS files are being loaded through the main pipeline in application.js.
Does anyone know why nothing happens when I try this?
Edit: more information - this is how main.js looks before build:
/* eslint-disable */
import Vue from 'vue'
// UI components
import VueSelect from 'vue-select'
import DynamicForm from './components/DynamicForm/'
Vue.component('vue-select', VueSelect)
Vue.config.productionTip = false
const DynamicForms = new Vue({
el: '.dynamic-form',
render: h => h(DynamicForm)
})
Edit: I managed to get Vue to work by integrating Webpack to Rails with Webpacker. However I still have some problems regarding context:
This is my main.js in one of the Vue components. It was working fine until I tried the PropData stunt so I could reuse the component with different data in a few places.
/* eslint-disable */
import Vue from 'vue'
// UI components
import VueSelect from 'vue-select'
// import 'nouislider'
import DynamicForm from './components/DynamicForm/'
import fields from './fields'
import fieldRules from './field-rules'
Vue.component('vue-select', VueSelect)
Vue.config.productionTip = false
document.addEventListener('DOMContentLoaded', () => {
const el = document.createElement('div')
document.querySelector('.dynamic-form').appendChild(el)
const vm = new DynamicForm({
propsData: {
fields,
fieldRules
},
el,
render: h => h(DynamicForm)
})
})
This is DynamicForm/index.vue
<template>
<div id='app'>
<ParamList :fields='paramFields' :fieldRules='paramRules'></ParamList>
<Output></Output>
</div>
</template>
<script>
import Vue from 'vue'
import ParamList from './ParamList'
import Output from './Output'
export default Vue.extend({
props: [ 'fields', 'fieldRules' ],
name: 'DynamicForm',
components: {
ParamList,
Output
},
data () {
return {
paramFields: this.fields,
paramRules: this.fieldRules
}
}
})
</script>
<style>
</style>
The field and fieldData props are merely JSON/JSONP with some data I'm going to use inside those components. The idea is that I could write another main.js changing just the field and fieldData when initing the Vue instance.
What am I doing wrong?
I've managed to fix everything in a three-step change to my components.
Integrate Webpack into Rails using Webpacker. There's even a Vue template!
Change the root component (the one mounted at a real DOM element) to a Vue subclass using Vue.extend (so the module line # the .vue file read export default Vue.extend({ instead of simply export default {
Remove the render function from the new DynamicForm (the name I assigned Vue.extend to) so it renders its own template.
I hope it helps as it was quite a pain to me!

VueJs Runtime components

I'm using VueJS with my current project and when I tried to register new component and use it like <component></component> i got that runtime error. I found solution to render components like this:
import App from './components/Example.vue'
new Vue({
el: '#app',
render: h => h(App)
})
And yes It works, it really renders my component but is there any way to stick with that method of registering components ?
I've read about script to add in webpack but I'm not really sure what and where to do this is my script for running code: (watching for changes too)
watchify src/app.js -t vueify -t babelify -p browserify-hmr -p [ vueify/plugins/extract-css -o public/styles.bundle.css ] -o public/app.bundle.js
There are two ways you can use the component in your template the way you want. First, you can register the component globally.
import SomeComponent from "./SomeComponent.vue"
Vue.component("some-component", SomeComponent)
You could then use this in a template as <some-component></some-component>.
Second, you can locally register components.
import SomeComponent from "./SomeComponent.vue"
new Vue({
components:{
SomeComponent
}
})
Which, again, you could use in the template as <some-component></some-component>, but only in the root Vue. If you wanted to use it in other components you would need to import it and add it to the local components of the component you want to use it in.

Categories