I have spent quite a lot time trying to mock imported component in the Cypress Component Testing.
Consider we have a Parent.vue component:
<template>
<ChildComponent />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
</script>
ChildComponent.vue:
<template>
<div data-cy="child">Child</div>
</template>
How can we mock it the cypress?
React
Before I go to the answer, there are several articles that can come in handy for React users. The first link has worked for me:
https://glebbahmutov.com/blog/stub-react-import/
https://github.com/cypress-io/cypress/tree/master/npm/react/cypress/component/advanced/mocking-imports
https://sinonjs.org/how-to/stub-dependency/
I have added additional rule to the webpack:
// cypress.config.js
devServer: {
framework: 'create-react-app',
bundler: 'webpack',
webpackConfig: {
mode: 'development',
devtool: false,
module: {
rules: [
// application and Cypress files are bundled like React components
// and instrumented using the babel-plugin-istanbul
// (we will filter the code coverage for non-application files later)
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-env', '#babel/preset-react'],
plugins: [
// we could optionally insert this plugin
// only if the code coverage flag is on
'istanbul',
[
'#babel/plugin-transform-modules-commonjs',
{ loose: true },
],
],
},
},
},
],
},
},
},
And in Parent.js I have mocked ChildComponent.js as following:
import Parent from './Parent.js';
import * as ChildComponent from './ChildComponent.js';
it('mocks', () => {
cy.stub(ChildComponent, 'default').returns(<div data-cy="child">MockedComponent</div>');
cy.mount(Parent);
cy.contains('[data-cy=child]', 'MockedComponent');
});
it('works without mock', () => {
cy.mount(Parent);
cy.contains('[data-cy=child]', 'Child');
});
Vue
If we want to mock the component in Vue, we don't need any changes in the webpack, instead of mocking the default import we need to mock render function, and to suppress execution of script tag in the mocked component, we can mock setup function.
Example:
import Parent from './Parent.vue';
import ChildComponent from './ChildComponent.vue';
it('should mock', () => {
cy.stub(ChildComponent, 'setup').value(() => {});
cy.stub(ChildComponent, 'render').value(() => <div data-cy="child">MockedComponent</div>);
cy.mount(Parent);
cy.contains('[data-cy=child]', 'MockedComponent');
});
Mocking js module:
If you want to mock js module default export, you will need to do the same modification to the webpack config as for React above.
import * as jsModule from './jsModule.js';
it('should be mocked', () => {
cy.stub(jsModule, 'default').value({
someProperty: 'newProperty',
});
});
Mocking a function
To mock just a function of the module, you can use returns or value;
import jsModule from './jsModule.js';
it('should mock with returns', () => {
cy.stub(jsModule, 'innerFunction').returns('MockValue');
});
it('should mock with value', () => {
cy.stub(jsModule, 'innerFunction').value(() => 'MockValue');
});
Related
Hello everyone I'm using vue 3 with storybook 6.5.16 and when i import the SVGs as a component using svg-inline-loader i get the following error in storybook app:
enter image description here
(Failed to execute 'createElement' on 'Document' svg is not a valid name)
Storybook main.js
const path = require('path');
module.exports = {
stories: [
'../src/**/*.stories.mdx',
'../src/**/*.stories.#(js|jsx|ts|tsx)',
],
addons: [
'#storybook/addon-links',
'#storybook/addon-essentials',
'#storybook/addon-interactions',
],
framework: '#storybook/vue3',
core: {
builder: '#storybook/builder-webpack5',
},
webpackFinal: async (config, { configType }) => {
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
config.module.rules.push({
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: "sass-loader",
options: {
additionalData: `
#import "#/assets/scss/main.scss";
`,
implementation: require('sass'),
},
},
],
});
(() => {
const ruleSVG = config.module.rules.find(rule => {
if (rule.test) {
const test = rule.test.toString();
const regular = /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
const regularString = regular.toString();
if (test === regularString) {
return rule;
}
}
});
ruleSVG.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
})();
config.module.rules.push({
test: /\.svg$/,
use: ['svg-inline-loader'],
});
config.resolve.alias['#'] = path.resolve('src');
return config;
},
}
package.json file
enter image description here
SVG Vue components
<template>
<div
ref="icon"
class="v-icon"
#click="onClick"
>
<component
:is="iconName"
class="v-icon__svg"
/>
</div>
</template>
<script>
import Cards from '#/assets/icons/Cards.svg';
export default {
name: 'VIcon',
components: {
Cards,
},
props: {
iconName: {
type: String,
required: true,
},
},
};
</script>
.babelrc file
{
"presets": ["#babel/preset-env", "#babel/preset-react"]
}
i tried to use vue-svg-loader to replace svg-inline-loader but it didn't work and I got another error while building the app
ModuleBuildError: Module build failed: Error: Cannot find module './Block'
I also tried to use babel-loader in conjunction with vue-svg-loader but unfortunately I also got an error:
enter image description here
has anyone come across this or can you show your use cases of using SVGs components in Storybook and Vue 3?
I have created a component as part of my component library that I am building with Vue3 and Vite. Everything works well, except when I try to use environment variables. I want the app that consumes this component library to be able to provide the component with environment specific data.
I have played around and found that if I have a .env file as part of the component library project, I am able to access those variables, but I want to be able to provide that during runtime and not during build time.
Here is my vite.config.ts
import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "#vitejs/plugin-vue";
import dts from "vite-plugin-dts";
export default ({ mode }) => {
return defineConfig({
optimizeDeps: {
exclude: ["vue-demi"],
},
plugins: [
vue(),
dts({
insertTypesEntry: true,
}),
],
server: {
open: true,
},
build: {
lib: {
entry: resolve(__dirname, "src/lib.ts"),
name: "complib",
fileName: "complib",
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "Vue",
},
exports: "named",
},
},
},
});
};
The entry looks like:
import { App, install } from "vue-demi";
import TestComp from "./components/TestComp.vue";
import "./tailwind.css";
install();
export default {
install: (app: App) => {
app.component("TestComp", TestComp);
},
};
export { Header };
And here is a minimal component TestComp.vue:
<script setup lang="ts">
import { onMounted } from "vue";
onMounted(() => {
console.log(import.meta.env.VITE_TEST_VAR);
});
</script>
<template>
<span>Test Comp</span>
</template>
I am currently coding a small vue component library for company's internal use. However, I faced some difficulties on building and deploy the project. This component library contains several components inside. And it will finally build by vue-cli-service build --target lib. The problem I am facing right now is that I have a component (i.e. called Audio.vue), and it will import a .mp3 file inside the component. The component is look like this
<template>
<audio controls :src="soundSrc" />
</template>
<script>
import { defineComponent } from 'vue';
import sound from './assets/sound.mp3';
export default defineComponent({
name: 'Audio',
props: {},
setup() {
return { soundSrc: sound };
},
});
</script>
<style scoped></style>
However, I use this component by serving (vue-cli-service serve") my project is fine. But if I build this project by running vue-cli-service build --target lib --name project-name-here. And I use this built version as a git submodule of my library in another vue project by importing project-name-here.common built before. The sound.mp3 import from './assets/sound.mp3' could not be found. It seems it is using a relative path of my another vue project (i.e. localhost:8080) instead of the library project
Here is my vue.config.js
const path = require('path');
module.exports = {
css: {
extract: false,
},
lintOnSave: false,
productionSourceMap: false,
configureWebpack: {
resolve: {
alias: {
mediaAsset: path.resolve(__dirname, './src/components/Audio/assets'),
},
},
},
chainWebpack: (config) => {
const imgRule = config.module.rule('images');
imgRule
.use('url-loader')
.loader('url-loader')
.tap((options) => Object.assign(options, { limit: Infinity }));
const svgRule = config.module.rule('svg');
svgRule.uses.clear();
svgRule
.test(/\.svg$/)
.use('svg-url-loader') // npm install --save-dev svg-url-loader
.loader('svg-url-loader');
config.module
.rule('raw')
.test(/\.txt$/)
.use('raw-loader')
.loader('raw-loader');
config.module
.rule('media')
.test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
.use('url-loader')
.loader('url-loader')
.tap((options) =>
Object.assign(options, {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]',
},
},
})
);
},
};
Appreciated for answering this question. Thanks.
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("...", () => {
...
});
});
TL;DR
Unit Test for component copied from Vuetify link are passing but my actual unit test for my component written in pug is not passing.
Details:
I am working on a project which is built using Vue, NUXT, Vuetify and pug as template language. I am configuring the project to write test cases with JEST test runner. The unit test is currently failing and I would need some steps to identify the problem.
I have read through following:
https://vue-test-utils.vuejs.org/guides/#getting-started
https://vuetifyjs.com/en/getting-started/unit-testing/
Following versions are in use:
nuxt: ^2.0.0
nuxtjs/vuetify: ^1.10.2
pug: ^2.0.4
pug-plain-loader: ^1.0.0
jest: ^24.1.0
vue-jest: ^4.0.0-0
Following is the relevant folder structure
<project root>/jest.config.js
<project root>/components/common/CustomCard.vue
<project root>/components/common/MessageDialog.vue
<project root>/tests/unit/components/common/TestMessageDialog.spec.js
<project root>/tests/unit/components/common/TestCustomCard.spec.js
Following is are seemingly relevant parts of the configurations:
jest.config.js
module.exports = {
verbose: true,
moduleNameMapper: {
'^#/(.*)$': '<rootDir>/$1',
'^~/(.*)$': '<rootDir>/$1',
'^vue$': 'vue/dist/vue.common.js'
},
moduleFileExtensions: ['js', 'vue', 'json'],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest'
},
'collectCoverage': false,
'collectCoverageFrom': [
'<rootDir>/components/**/*.vue',
'<rootDir>/pages/**/*.vue'
],
globals: {
'vue-jest': {
pug: {
doctype: 'html'
}
}
},
preset: '#vue/cli-plugin-unit-jest/presets/no-babel'
}
TestMessageDialog.spec.js
import {
mount,
createLocalVue
} from '#vue/test-utils'
import Vuetify from 'vuetify'
import MessageDialog from '#/components/common/MessageDialog.vue'
describe('MessageDialog.vue', () => {
const sampleData = {
titleText: 'title text with unique id : 2020.03.12.00'
}
let wrappedMessageDialog
beforeEach(() => {
const vuetify = new Vuetify()
const localVue = createLocalVue()
wrappedMessageDialog = mount(
MessageDialog,
{
localVue,
vuetify,
propsData: sampleData
}
)
})
it('renders correctly when passed props', () => {
expect(
wrappedMessageDialog
.find({ name: 'v-card-title' })
.text()
).toMatch(sampleData.titleText)
})
})
MessageDialog.vue
<template lang="pug">
v-dialog(v-model="isVisible" width="600")
v-card
v-card-title {{ titleText }}
</template>
<script>
export default {
props: {
titleText: {
default: '',
type: String
}
}
}
</script>
I am getting following error
FAIL tests/unit/components/common/TestMessageDialog.spec.js
[vue-test-utils]: find did not return Component, cannot call text() on empty Wrapper
31 | wrappedMessageDialog
32 | .find({ name: 'v-card-title' })
> 33 | .text()
| ^
34 | ).toMatch(sampleData.titleText)
35 | })
36 | })
at throwError (node_modules/#vue/test-utils/dist/vue-test-utils.js:1709:9)
at ErrorWrapper.text (node_modules/#vue/test-utils/dist/vue-test-utils.js:8767:3)
at Object.it (tests/unit/components/common/TestMessageDialog.spec.js:33:10)
console.error node_modules/vue/dist/vue.common.dev.js:630
[Vue warn]: Unknown custom element: <v-dialog> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
found in
---> <Anonymous>
<Root>
Similar warnings for <v-dialog>, <v-card> & <v-card-title>
Looks like you will want to register the components in the MessageDialog component. E.g.
<script>
import Foo from './Foo.vue;
...
export default {
components: { Foo }
...
}
...
</script>
More info: https://v2.vuejs.org/v2/guide/components-registration.html#Local-Registration