How to mock SVG's when snapshot testing with vitest and Storybook? - testing

I am trying to run vitest snapshot tests on Storybook stories using the composeStories Fn from #storybook/testing-react, but I keep getting the error:
FAIL src/components/common/Nav/Nav.test.tsx > Nav Component > it should match the snapshot
Error: Element type is invalid: expected a string (for built-in components) or a class/function
(for composite components) but got: undefined. You likely forgot to export your component from
the file it's defined in, or you might have mixed up default and named imports.
Check the render method of `Nav`.
//... stack trace
I believe it's related to the svg imports, as this only occurs in components that import svgs as react components via the SVGR library. i.e.
// components/common/Nav.tsx
import { ReactComponent as ECDLogo } from '#assets/ecd_logo.svg';
And my vite.config.ts uses the vite-svgr-plugin:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '#vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
import path from 'path';
const tsConfigPathsOpts = {
extensions: ['.svg', '.png', '.jpeg'],
loose: true,
};
export default defineConfig({
build: {
outDir: 'build',
},
define: {
global: {},
},
resolve: {
alias: {
'#': path.resolve(__dirname, './src'),
'#assets': path.resolve(__dirname, './src/assets'),
'#styles': path.resolve(__dirname, './src/styles'),
'#types': path.resolve(__dirname, './src/types'),
'#components': path.resolve(__dirname, './src/components'),
},
},
plugins: [react(), svgr(), tsconfigPaths(tsConfigPathsOpts)],
});
My Storybook config (.storybook/main.js) looks like so:
const path = require('path');
const { mergeConfig } = require('vite');
const tsconfigPaths = require('vite-tsconfig-paths');
const svgr = require('vite-plugin-svgr');
const tsConfigPathsOpts = {
extensions: ['.svg', '.png', '.jpeg'],
loose: true,
};
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.#(js|jsx|ts|tsx)'],
addons: [
'#storybook/addon-essentials',
'#storybook/preset-create-react-app',
'#storybook/addon-a11y',
'#storybook/node-logger',
'storybook-addon-designs',
'storybook-color-picker',
'storybook-dark-mode',
],
framework: '#storybook/react',
core: {
builder: '#storybook/builder-vite',
},
async viteFinal(config, { configType }) {
return mergeConfig(config, {
resolve: {
alias: {
'#': path.resolve(__dirname, '../src'),
'#assets': path.resolve(__dirname, '../src/assets'),
'#styles': path.resolve(__dirname, '../src/styles'),
'#types': path.resolve(__dirname, '../src/types'),
'#components': path.resolve(__dirname, '../src/components'),
},
},
plugins: [svgr(), tsconfigPaths.default(tsConfigPathsOpts)],
});
},
};
I've come to understand that I need to mock these SVG's so that their snapshot is consistent, but I need direction on whether my mocking implementation is correct. See the vi.mock Fn below.
// components/common/Nav/Nav.test.tsx
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import { composeStories } from '#storybook/testing-react';
import * as stories from './Nav.stories'; // import all stories from the stories file
import { vi } from 'vitest';
const { NavDefault } = composeStories(stories);
👀
vi.mock('#assets/*', () => {
return {
default: 'SVGUrl',
ReactComponent: 'div',
};
});
describe('Nav Component', () => {
test('it should match the snapshot', () => {
const { asFragment } = render(<NavDefault />);
expect(asFragment()).toMatchSnapshot();
});
});
I was expecting this to mock all the imports from #assets/* to be strings "SVGUrl" or 'div'
But I get the same error as above:
FAIL src/components/common/Nav/Nav.test.tsx > Nav Component > it should match the snapshot
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

we had the same issue today and fixed it by adding the plugin svgr() in vitest.config.ts
import svgr from "vite-plugin-svgr";
export default defineConfig({
plugins: [
// ...other plugins
svgr(),
]
})

Related

Load SVGs components in storybook VUE not working

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?

How to use Environment Variables inside Vue3+Vite component library?

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>

Using vue slots in library gives currentRenderingInstance is null

I am creating a Vue component library with Rollup, but when I use slots it gives me the following error:
Uncaught (in promise) TypeError: currentRenderingInstance is null
I made a very simple component in my library:
<script setup></script>
<template>
<button>
<slot></slot>
</button>
</template>
<style scoped></style>
Then I simply use it like this:
<ExampleComponent>
Text
</ExampleComponent>
If I remove the slot and replace it with a prop or hard-coded text, everything works fine.
This is my rollup.config.js:
import { defineConfig } from 'rollup';
import path from 'path';
import resolve from '#rollup/plugin-node-resolve';
import commonjs from '#rollup/plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import vue from 'rollup-plugin-vue';
// the base configuration
const baseConfig = {
input: 'src/entry.js',
};
// plugins
const plugins = [
vue(),
resolve({
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
}),
// process only `<style module>` blocks.
postcss({
modules: {
generateScopedName: '[local]___[hash:base64:5]',
},
include: /&module=.*\.css$/,
}),
// process all `<style>` blocks except `<style module>`.
postcss({ include: /(?<!&module=.*)\.css$/ }),
commonjs(),
];
const external = ['vue'];
const globals = {
vue: 'Vue',
};
export default [
// esm
defineConfig({
...baseConfig,
input: 'src/entry.esm.js',
external,
output: {
file: 'dist/vue-my-lib.esm.js',
format: 'esm',
exports: 'named',
},
plugins,
}),
// cjs
defineConfig({
...baseConfig,
external,
output: {
compact: true,
file: 'dist/vue-my-lib.ssr.js',
format: 'cjs',
name: 'VueMyLib',
exports: 'auto',
globals,
},
plugins,
}),
// iife
defineConfig({
...baseConfig,
external,
output: {
compact: true,
file: 'dist/vue-my-lib.min.js',
format: 'iife',
name: 'VueMyLib',
exports: 'auto',
globals,
},
plugins,
}),
];
Any idea about the problem?
After a whole day of searching, I found the solution (here and here). It's a problem with using a library locally (e.g., through npm link) where it seems there are two instances of Vue at the same time (one of the project and one of the library). So, the solution is to tell the project to use specifically its own vue through webpack.
In my case, I use Jetstream + Inertia, so I edited webpack.mix.js:
const path = require('path');
// ...
mix.webpackConfig({
resolve: {
symlinks: false,
alias: {
vue: path.resolve("./node_modules/vue"),
},
},
});
Or if you used vue-cli to create your project, edit the vue.config.js:
const { defineConfig } = require("#vue/cli-service");
const path = require("path");
module.exports = defineConfig({
// ...
chainWebpack(config) {
config.resolve.symlinks(false);
config.resolve.alias.set("vue", path.resolve("./node_modules/vue"));
},
});
Thanks to #mikelplhts
On vite + esbuild I used:
export default defineConfig({
...
resolve: {
alias: [
...
{
find: 'vue',
replacement: path.resolve("./node_modules/vue"),
},
],
},
...

Dependency not found - Vue Js

I have recently added axios to a file called services.js so it's better organised. This file is on my root folder.
#/services.js
import axios from "axios";
const axiosInstance = axios.create({
baseURL: " server url here",
});
export const api = {
get(endpoint) {
return axiosInstance.get(endpoint);
},
post(endpoint, body) {
return axiosInstance.post(endpoint, body);
},
};
Then I have a component called Post.vue in my view folder:
<template>
<section>
<div>
<ul></ul>
</div>
</section>
</template>
<script>
import { api } from "#/services.js";
export default {
name: "Post",
props: ["id"],
data() {
return {
post: null,
};
},
methods: {
getPost() {
api.get(`/post/${this.id}`).then(response => {
this.post = response.data;
console.log(this.post);
});
},
},
created() {
this.getPost();
},
};
</script>
<style></style>
I also have a router.ts file with all my routes:
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Home from "../views/Home.vue";
import Podcasts from "../views/Podcasts.vue";
import Post from "../views/Post.vue";
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/podcasts",
name: "podcasts",
component: Podcasts,
},
{
path: "/post/:id",
name: "post",
component: Post,
props: true,
},
],
});
export default router;
It's giving me a dependency error like #/services.js did not exist.
Unsure what's wrong at this stage.
Thanks a lot in advance for helping out
In a standard Vue CLI project, the # symbol resolves to /src
If your file is in the root of your project try
import { api } from '#/../services'
But personally, I'd move it into src
You can check the Webpack configuration using
vue inspect
Look for the resolve.alias rules.
Check your webpack configuration, depends on the version of webpack you have, there should be an alias # like this:
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
"#": path.resolve(__dirname) // check the path here
}
}
};
Or if you are using vue.config.js
configureWebpack: {
name: name,
resolve: {
alias: {
'#': path.resolve(__dirname)// check the path here
}
}
},
Make sure the path is correctly set up. You mentioned you have another project working fine, which makes it a good reference.

How can I configure my Webpack file in Storybook correctly to compile my CSS with PostCSS?

I have a Vue CLI up and running successfully with PostCSS and all of the plugins I require. However, when it comes to setting up the same PostCSS config for Storybook, no matter which combination of things I try from various sources that I've come across, I cannot get my Webpack file to compile correctly.
.storybook/postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
'postcss-nested': {},
'postcss-custom-media': {},
'postcss-advanced-variables': {},
},
};
.storybook/webpack.config.js
const path = require('path');
const rootPath = path.resolve(__dirname, '../src');
module.exports = ({ config, mode }) => {
config.module.rules.push({
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
'postcss-loader',
],
});
config.resolve.alias['#'] = rootPath;
return config;
};
./storybook.config.js
import { configure } from '#storybook/vue';
import Vue from 'vue';
import Vuex from 'vuex';
import '../src/css/main.css';
import '../src/stories/main.css';
Vue.use(Vuex);
function loadStories() {
require('../src/stories');
}
configure(loadStories, module);
Error
ERROR in ./src/css/main.css (./node_modules/#storybook/core/node_modules/css-loader/dist/cjs.js??ref--3-1!./node_modules/postcss-loader/src??postcss!./node_modules/style-loader!./node_modules/css-loader??ref--6-1!./node_modules/postcss-loader/src!./src/css/main.css)
Module build failed (from ./node_modules/postcss-loader/src/index.js):
SyntaxError
(2:1) Unknown word
1 |
> 2 | var content = require("!!../../node_modules/css-loader/index.js??ref--6-1!../../node_modules/postcss-loader/src/index.js!./main.css");
| ^