Can't mock module import in Cypress Vue component tests - vue.js

I'm new to Cypress component testing, but I was able to set it up easily for my Vue project. I'm currently investigating if we should replace Jest with Cypress to test our Vue components and I love it so far, there is only one major feature I'm still missing: mocking modules. I first tried with cy.stub(), but it simply didn't work which could make sense since I'm not trying to mock the actual module in Node.js, but the module imported within Webpack.
To solve the issue, I tried to use rewiremock which is able to mock Webpack modules, but I'm getting an error when running the test:
I forked the examples from Cypress and set up Rewiremock in this commit. Not sure what I'm doing wrong to be honest.
I really need to find a way to solve it otherwise, we would simply stop considering Cypress and just stick to Jest. If using Rewiremock is not the way, how am I suppose to achieve this? Any help would be greatly appreciated.

If you are able to adjust the Vue component to make it more testable, the function can be mocked as a component property.
Webpack
When vue-loader processes HelloWorld.vue, it evaluates getColorOfFruits() and sets the data property, so to mock the function here, you need a webpack re-writer like rewiremock.
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
colorOfFruits: getColorOfFruits(), // during compile time
};
},
...
Vue created hook
If you initiialize colorOfFruits in the created() hook, you can stub the getColorOfFruits function after import but prior to mounting.
HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>{{ colorOfFruits.apple }}</h2>
<p>
...
</template>
<script lang="ts">
import { getColorOfFruits } from "#/helpers/fruit.js";
export default {
name: "HelloWorld",
getColorOfFruits, // add this function to the component for mocking
props: {
msg: String,
},
data() {
return {
colorOfFruits: {} // initialized empty here
};
},
created() {
this.colorOfFruits = this.$options.colorOfFruits; // reference function saved above
}
});
</script>
HelloWorld.spec.js
import { mount } from "#cypress/vue";
import HelloWorld from "./HelloWorld.vue";
it("mocks an apple", () => {
const getMockFruits = () => {
return {
apple: "green",
orange: "purple",
}
}
HelloWorld.getColorOfFruits = getMockFruits;
mount(HelloWorld, { // created() hook called as part of mount()
propsData: {
msg: "Hello Cypress!",
},
});
cy.get("h1").contains("Hello Cypress!");
cy.get("h2").contains('green')
});

We solved this by using webpack aliases in the Cypress webpack config to intercept the node_module dependency.
So something like...
// cypress/plugins/index.js
const path = require("path");
const { startDevServer } = require('#cypress/webpack-dev-server');
// your project's base webpack config
const webpackConfig = require('#vue/cli-service/webpack.config');
module.exports = (on, config) => {
on("dev-server:start", (options) => {
webpackConfig.resolve.alias["your-module"] = path.resolve(__dirname, "path/to/your-module-mock.js");
return startDevServer({
options,
webpackConfig,
})
});
}
// path/to/your-module-mock.js
let yourMock;
export function setupMockModule(...) {
yourMock = {
...
};
}
export default yourMock;
// your-test.spec.js
import { setupMock } from ".../your-module-mock.js"
describe("...", () => {
before(() => {
setupMock(...);
});
it("...", () => {
...
});
});

Related

Convert Vue2 component (factory) to Vue3 with render function

I would like to convert an existing Vue2 component to Vue3 but I'm having problems. Basically what you see below is what I come up so far (with irrelevant code left out). I could share the Vue2 code as well but it does not look very different.
When running the app I get the error Component is missing template or render function. So what could be the issue here? Is it because there is possibly a different instance of Vue being defined with createApp?
I think the inner workings of the mounted and other hooks are not that important right now. What is puzzling me is that the render function is not even called. I checked that with a log statement.
import { createApp, h } from "vue";
const app = createApp({});
export default (Component, tag = "span") =>
app.component("adaptor", {
render() {
return h(tag, {
ref: "container",
props: this.$attrs,
});
},
data() {
return {
comp: null,
};
},
mounted() {
this.comp = new Component({
target: this.$refs.container,
props: this.$attrs,
});
},
updated() {
this.comp.$set(this.$attrs);
},
unmounted() {
this.comp.$destroy();
},
});
It basically provides a wrapper to render components of other frameworks in Vue. It is used like this in Vue2 at the moment:
import toVue from "../toVue";
[...]
{ components: { MyComp: toVue(MyOtherFrameworkComp) }
[...]

Vue.js Pass plugin variable towards custom component

I am a novice in Vue.js and I am trying to create my first plugin, something very simple, to be able to call it from my application as follows:
app.js
Vue.use(Pluginify, { option1: 1, option2: 2 });
The problem I am having is the following, I have in my index.js file of my plugin, a variable called configuration, this variable is the second argument that I passed to the Vue.use function, the problem is that I need to pass that variable to a custom component I'm creating, but can't. Please could you help me, this is the structure I have so far:
index.js
import Vue from 'vue';
import Plugin from './Plugin';
import { isObject } from 'lodash';
export const instance = new Vue({
name: 'Plugin',
});
const Pluginify = {
install(Vue, configuration = {}) { // This variable here, I need to pass it to the ```Plugin.vue``` component
Vue.component('plugin', Plugin);
const pluginify = params => {
if (isObject(params)) {
instance.$emit('create', params);
}
};
Vue.pluginify = pluginify;
Vue.prototype.$pluginify = pluginify;
}
};
export default Pluginify;
I have a component called Plugin that is empty, but I need it to contain the configuration object in order to use its values ​​in the future.
Plugin.vue
<template>
</template>
<script>
import { instance } from './index';
export default {
name: 'Plugin',
mounted() {
instance.$on('create', this.create);
},
methods: {
create(params) {
}
}
};
</script>
<style scoped>
</style>
Thank you very much in advance
Ok, I have achieved it as follows:
index.js
const Pluginify = {
install(Vue, configuration = {}) {
/**
* Default plugin settings
*
* #type {Object}
*/
this.default = configuration;
....
So in my Plugin.vue component:
import * as plugin from './index';
So I can call in any method of my component the configuration parameters as follows:
...
mounted() {
console.log(plugin.default.option1);
},
...
I hope I can help anyone who gets here with a similar question.
It seems like this listener is added after the create event already fired
mounted() {
instance.$on('create', this.create);
},
You could use a global variable in your plugin like this ...
Vue.prototype.$pluginifyConfig = configuration;
and then you can call it with this.$pluginifyConfig in the plugin, or anywhere else.
But that pollutes the global scope
Maybe this could help:
const Plugin = {
template: '<div>Plugin</div>',
data() {
return {
a: 'a data'
};
},
mounted() {
console.log(this.a);
console.log(this.message);
}
};
const Pluginify = {
install(Vue, options) {
Vue.component('plugin', {
extends: Plugin,
data() {
return {...options}
}
});
}
};
Vue.use(Pluginify, {message: 'hello, world!'});
new Vue({
el: '#app'
});

Quasar Unknown custom element error in unit test

I have a simple Vue component that uses Quasar button
<template>
<div>
<span class="count">{{ count }}</span>
<q-btn #click="increment">Increment</q-btn>
</div>
</template>
<script>
export default {
name: 'TestComponent',
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count += 1;
},
},
};
</script>
I create a unit test for it
import { mount, createLocalVue } from '#vue/test-utils';
import { Quasar, QBtn } from 'quasar';
import TestComponent from '../TestComponent';
describe('TestComponent', () => {
let wrapper;
beforeEach(() => {
const localVue = createLocalVue();
localVue.use(Quasar, { components: { QBtn } });
wrapper = mount(TestComponent, { localVue });
});
it('renders the correct markup', () => {
expect(wrapper.html()).toContain('<span class="count">0</span>');
});
// it's also easy to check for the existence of elements
it('has a button', () => {
expect(wrapper.contains('button')).toBe(true);
});
});
My problem:
If I run the test cases (it function) one by one at a time the test will pass. For example, remove the second it('has a button'...) then run the test. It'll pass. It's the same when removing the first it('renders the correct markup'...)
However, If I keep all test cases then run the test. The second test case will fail with an error
console.error node_modules/vue/dist/vue.common.dev.js:630
[Vue warn]: Unknown custom element: <q-btn> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
found in
---> <TestComponent>
<Root>
What am I doing wrong?
Try removing the before-each. I saw this problem too. Can't remember what exactly fixed it but this is how I have my describe block.
describe('Mount Quasar', () => {
const localVue = createLocalVue()
localVue.use(Quasar, { components })
const wrapper = shallowMount(Register, {
localVue,
stubs: ['router-link', 'router-view']
})
const vm = wrapper.vm
it('passes the sanity check and creates a wrapper', () => {
expect(wrapper.isVueInstance()).toBe(true)
})
})
You will need to import quasar into either webpack, babel, or jest.
In the jest.config.js file
Add
moduleNameMapper: {
quasar: "quasar-framework/dist/umd/quasar.mat.umd.min.js"
},

How to mock $parent for vue-components

How am i able to mock $parent for my specs? When using shallowMount with my component i always get clientWidth/clientHeight of undefined. I already tried mocking $parent as an object with an $el as a key and two more nested keys for clientWidth and clientHeight, but that's not working as expected. I cannot figure out the right usage of parentComponent.
I've got a single file component as seen below:
<template>
<img :src="doSomething">
</template>
<script>
export default {
name: "Foobar",
data() {
return {
size: null
};
},
computed: {
doSomething() {
# here is some string concatenation etc.
# but not necessary for example
return this.size;
}
},
created() {
let parent = this.$parent.$el;
this.size = `size=${parent.clientWidth}x${parent.clientHeight}`;
}
};
</script>
creating the vue app looks like this:
import Vue from "vue";
import Foobar from "./Foobar";
const vueEl = "[data-vue-app='foobar']";
if (document.querySelector(vueEl)) {
new Vue({
el: vueEl,
components: {
"foo-bar": Foobar
}
});
}
and the combination of using slim with my component looks like this:
div data-vue-app="foobar"
foo-bar
This is my test setup:
import { shallowMount } from "#vue/test-utils";
import Foobar from "#/store/Foobar";
describe("Foobar.vue", () => {
let component;
beforeEach(() => {
component = shallowMount(Foobar, {});
});
The parentComponent mounting option expects a component object:
import Parent from "../Parent.vue";
...
beforeEach(() => {
component = shallowMount(Foobar, {
parentComponent: Parent
});
});
Also, try pinning the version of vue-test-utils to "^1.0.0-beta.28". This should allow your tests to complete, but the clientWidth/Height will still be 0x0
ref: https://vue-test-utils.vuejs.org/api/options.html#parentcomponent

Vue + Express(NestJS) Impossible task, You are using the runtime-only build of Vue where

It's making it impossible for me to add Vue as a view system to a framework called Nest with Express.
I didn't think that adapting Vue was so complicated. That's why I'm here so that you can guide me on the right path and I won't use Vue directly.
Fist the error:
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
(found in <Root>)
app.controller.ts
import { Controller, Get, Render, Response } from '#nestjs/common';
import { createRenderer } from 'vue-server-renderer';
import { createApp } from './app';
import HelloComponent from './components/Hello';
const context = {data: {}, view: '', componets: {} };
#Controller()
export class AppController {
#Get()
getHello(#Response() res): any {
context.data = { message: 'Esto es un nuevo mensaje 2' };
context.componets = { 'hello' : HelloComponent };
const app = createApp(context);
const renderer = createRenderer();
renderer.renderToString(app, (err, html) => {
res.end(html);
});
}
}
import { createApp } from './app';
import Vue from 'vue';
export function createApp(context: any) {
return new Vue({
data: context.data,
template: fs.readFileSync('./index.html', 'utf-8'),
components: context.components,
}).$mount('#app');
}
I try is to have a base template and then add the components for each controller or route with NestJS.
I don't know if this is possible and if I'm forced to use Webpack, since I'm not currently using it.
Thanks!
Vue launched an entire site to walk you through getting your server side rendering up and running. It is NOT the same process that is outlined at https://vuejs.org.
Complete information can be found at: https://ssr.vuejs.org/ and is referenced in the main guide about halfway down the sidebar navigation under the heading serverside rendering https://v2.vuejs.org/v2/guide/ssr.html
Here is the gist of it to get you started:
npm install express --save
npm install vue vue-server-renderer --save
Integrating with your server example
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>The visited URL is: {{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
Rendering a Vue Instance
// Step 1: Create a Vue instance
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello World</div>`
})
// Step 2: Create a renderer
const renderer = require('vue-server-renderer').createRenderer()
// Step 3: Render the Vue instance to HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
// => <div data-server-rendered="true">Hello World</div>
})
// in 2.5.0+, returns a Promise if no callback is passed:
renderer.renderToString(app).then(html => {
console.log(html)
}).catch(err => {
console.error(err)
})
Thankfully it's not that complicated of an issue.
You are attempting to use the runtime build on .ts files, which you cannot. This is because only *.vue because they are pre-compiled.
To get around this, simply create an alias to vue in webpack:
resolve: {
alias: {
vue: 'vue/dist/vue.js'
}
}
Which will give you access to the template-compiler allowing you to use Vue inside of non pre-compiled templates (read: any file not ending in .vue)