I have the same issue than this post: Why does vitest mock not catch my axios get-requests?
I would like to test my vuex store on vuejs and it works for getters etc but not for actions part with axios get request.
I don't know if it's a good practice to test vuex store than the component in Vue ?
But I guess I need to test both, right ?
a project https://stackblitz.com/edit/vitest-dev-vitest-nyks4u?file=test%2Ftag.spec.js
my js file to test tag.js
import axios from "axios";
const actions = {
async fetchTags({ commit }) {
try {
const response = await axios.get(
CONST_CONFIG.VUE_APP_URLAPI + "tag?OrderBy=id&Skip=0&Take=100"
);
commit("setTags", response.data);
} catch (error) {
console.log(error);
return;
}
},
};
export default {
state,
getters,
actions,
mutations,
};
then my test (tag.spec.js)
import { expect } from "chai";
import { vi } from "vitest";
import axios from "axios";
vi.mock("axios", () => {
return {
default: {
get: vi.fn(),
},
};
});
describe("tag", () => {
test("actions - fetchTags", async () => {
const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Andrew" },
];
axios.get.mockImplementation(() => Promise.resolve({ data: users }));
axios.get.mockResolvedValueOnce(users);
const commit = vi.fn();
await tag.actions.fetchTags({ commit });
expect(axios.get).toHaveBeenCalledTimes(1);
expect(commit).toHaveBeenCalledTimes(1);
});
});
It looks like some other peolpe have the same issues https://github.com/vitest-dev/vitest/issues/1274 but it's still not working.
I try with .ts too but I have exactly the same mistake:
FAIL tests/unit/store/apiObject/tag.spec.js > tag > actions - fetchTags
AssertionError: expected "spy" to be called 1 times
❯ tests/unit/store/apiObject/tag.spec.js:64:24
62| await tag.actions.fetchTags({ commit });
63|
64| expect(axios.get).toHaveBeenCalledTimes(1);
| ^
65| expect(commit).toHaveBeenCalledTimes(1);
66| });
Expected "1"
Received "0"
Thanks a lot for your help.
I finally found the mistake, it was on my vitest.config.ts file, I have to add my global config varaible for my api: import { config } from "#vue/test-utils";
import { defineConfig } from "vitest/config";
import { resolve } from "path";
var configApi = require("./public/config.js");
const { createVuePlugin } = require("vite-plugin-vue2");
const r = (p: string) => resolve(__dirname, p);
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
},
define: {
CONST_CONFIG: configApi,
},
plugins: [createVuePlugin()],
resolve: {
alias: {
"#": r("."),
"~": r("."),
},
// alias: {
// "#": fileURLToPath(new URL("./src", import.meta.url)),
// },
},
});
two errors help me build with vuex modules
errors: unknown mutation type: setLoggedIn & unknown local mutation type: setLoggedIn, global type: auth/setLoggedIn
vuex version "vuex": "^4.0.0"
the problem occurs in the setLoggedInState(ctx) function
index.js
import Vuex from 'vuex'
import middleware from "./modules/middleware.js";
import auth from "./modules/auth";
export default new Vuex.Store({
namespaced: true,
modules: {
auth,
middleware
}
})
auth.js
const state = {
isLoggedIn: true,
};
const mutation = {
setLoggedIn(state, payload, ) {
state.isLoggedIn = payload;
},
};
const actions = {
setLoggedInState(ctx) {
return new Promise((resolve) => {
if (localStorage.getItem('token')) {
ctx.commit('setLoggedIn', true, {root: true});
resolve(true)
} else {
ctx.commit('setLoggedIn', false, {root: true});
resolve(false)
}
});
},
}
const getters = {
loggedIn(state) {
return state.isLoggedIn;
},
export default {
namespaced: true,
state,
mutation,
actions,
getters
}
Dashboard
import {mapActions} from 'vuex'
export default {
name: "Dashboard",
data: () => ({}),
created() {
this.checkUserState();
},
methods: {
...mapActions({
checkUserState: 'auth/setLoggedInState',
}),
I don’t understand how to fix errors I tried many ways I hope for your help
When you learn something new, please check for missing '; , .' etc.
And be sure that you write 'const mutations' not 'const mutation', following the documentation saving hours))
Hi I am using i18next with React Native for translations, but am getting the following error when trying to load json files:
i18next::backendConnector: lloading namespace translation for language en failed failed loading ./locales/en/translation.json
and also
i18next::translator: missingKey en translation screens.login.header screens.login.header
app/i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Fetch from 'i18next-fetch-backend';
const languageDetector = {
type: 'languageDetector',
async: true,
detect: cb => cb('en'),
init: () => {},
cacheUserLanguage: () => {},
};
i18n
.use(Fetch)
.use(languageDetector)
.use(initReactI18next)
.init({
lng: 'en',
debug: true,
interpolation: {
escapeValue: false,
formatSeparator: ',',
},
keySeparator: '.',
whitelist: ['en'],
nonExplicitWhitelist: true,
fallbackLng: 'en',
backend: {
loadPath: './locales/{{lng}}/{{ns}}.json',
allowMultiLoading: true,
},
react: {
wait: true,
},
});
export default i18n;
app/locales/en/translation.json
{
"screens": {
"login": {
"header": "Login to your your account",
"loginButton": "Login"
}
}
}
app/screens/auth/LoginScreen.js
{t('screens.login.header')}
Has anyone had any success using this library with React Native?
I have a component story that requires an API call performed by an ACTION from my Vuex store. However, the store can't be found by Storybook: Unhandled promise rejection TypeError: "this.$store is undefined".
I've tried to access the store through the created and mounted Vue lifecycle hooks but each of them returned undefined.
My Vuex store is correctly working inside my app.
I run on storybook 5.0.1 and vuex 3.1.1.
Here's my storybook config.js:
// Taken from https://davidwalsh.name/storybook-nuxt & https://github.com/derekshull/nuxt-starter-kit-v2/blob/master/.storybook/config.js
import { addParameters, configure } from '#storybook/vue';
import { withOptions } from '#storybook/addon-options';
import { setConsoleOptions } from '#storybook/addon-console';
import { create } from '#storybook/theming';
import Vue from 'vue';
import VueI18n from 'vue-i18n';
// Vue plugins
Vue.use(VueI18n);
setConsoleOptions({
panelExclude: [],
});
// Option defaults:
addParameters({
options: {
/**
* show story component as full screen
* #type {Boolean}
*/
isFullScreen: false,
/**
* display panel that shows a list of stories
* #type {Boolean}
*/
showNav: true,
/**
* display panel that shows addon configurations
* #type {Boolean}
*/
showPanel: true,
/**
* where to show the addon panel
* #type {String}
*/
panelPosition: 'bottom',
/**
* sorts stories
* #type {Boolean}
*/
sortStoriesByKind: false,
/**
* regex for finding the hierarchy separator
* #example:
* null - turn off hierarchy
* /\// - split by `/`
* /\./ - split by `.`
* /\/|\./ - split by `/` or `.`
* #type {Regex}
*/
hierarchySeparator: /\/|\./,
/**
* regex for finding the hierarchy root separator
* #example:
* null - turn off multiple hierarchy roots
* /\|/ - split by `|`
* #type {Regex}
*/
hierarchyRootSeparator: /\|/,
/**
* sidebar tree animations
* #type {Boolean}
*/
sidebarAnimations: true,
/**
* enable/disable shortcuts
* #type {Boolean}
*/
enableShortcuts: true,
/**
* theme storybook, see link below
*/
theme: create({
base: 'light',
brandTitle: '',
brandUrl: '',
// To control appearance:
// brandImage: 'http://url.of/some.svg',
}),
},
});
const req = require.context('../src/components', true, /\.story\.js$/)
function loadStories() {
req.keys().forEach((filename) => req(filename))
}
configure(loadStories, module);
Here's my component's story:
import { storiesOf } from '#storybook/vue';
import { withReadme } from 'storybook-readme';
import { withKnobs } from '#storybook/addon-knobs';
import HandoffMainView from './HandoffMainView.vue';
import readme from './README.md';
storiesOf('HandoffMainView', module)
.addDecorator(withReadme([readme]))
.addDecorator(withKnobs)
.add('Default', () => {
/* eslint-disable */
return {
components: { HandoffMainView },
data() {
return {
isLoading: true,
component: {
src: '',
data: [],
},
};
},
template: '<handoff-main-view :component="component" />',
};
});
Here's my component:
<template>
<main class="o-handoff-main-view">
<div class="o-handoff-main-view__content">
<div
:class="[
'o-handoff-main-view__background',
background ? `o-handoff-main-view__background--${background}` : false
]"
>
<loader
v-if="isLoading"
:color='`black`'
class="o-handoff-main-view__loader"
/>
<div
v-else
class="o-handoff-main-view__ui-component"
:style="getUiComponentStyle"
>
<img
:src="uiComponent.src"
alt=""
>
<handoff-main-view-layer-list
:layers="uiComponent.data"
/>
</div>
</div>
</div>
<div class="o-handoff-main-view__controls">
<handoff-main-view-zoom-handler
:default-zoom-level="zoomLevel"
:on-change="updateZoomLevel"
/>
</div>
</main>
</template>
<script>
import { mapActions } from 'vuex';
import Loader from '../../01-atoms/Loader/Loader.vue';
import HandoffMainViewZoomHandler from '../HandoffMainViewZoomHandler/HandoffMainViewZoomHandler.vue';
import HandoffMainViewLayerList from '../HandoffMainViewLayerList/HandoffMainViewLayerList.vue';
export default {
components: {
Loader,
HandoffMainViewZoomHandler,
HandoffMainViewLayerList,
},
props: {
background: {
type: String,
default: 'damier',
},
component: {
type: Object,
required: true,
},
},
data() {
return {
isLoading: true,
zoomLevel: 1,
uiComponent: {
src: null,
}
};
},
mounted() {
this.setUiComponentImage();
},
methods: {
...mapActions('UiComponent', [
'ACTION_LOAD_SIGNED_URLS'
]),
async setUiComponentImage() {
const uiComponentImg = new Image();
const signedUrls = await this.ACTION_LOAD_SIGNED_URLS([this.component.id]);
uiComponentImg.onload = () => {
this.isLoading = false;
};
uiComponentImg.src = this.uiComponent.src;
},
},
};
</script>
I bet somewhere in your app, probably main.js, you're doing something like:
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state,
mutations,
getters,
});
And then, when creating the Vue app, your calling new Vue({store, i18n...}).
You're already forging Vue with the ' i18n' module in your config.js. You would need to import Vuex and the store there too.
Now, having to import your store -or mock it- in your storybook setup may be a smell of your components being too large, or being too coupled with your store.
Usually, storybook is more intended to show components that display stuff (form controls, list of things... ) that have a dedicated functionality. Such components usually communicate with the rest of your application via props and events. Let's call this presentational components.
On the contrary, components that communicates with a store are usually views or pages, and they orchestrate the state and talk with the backend, and supply data to the former.
I think you should display on the storybook showcase only presentational components, and avoid talking global modules within them. At least, I believe this is the spirit behind storybook and how it is mainly used. That may be the reason because you don't find much docs about how to mock your store in storybook: storybook projects usually don't connect to vuex in the first place, I think.
pass new store instance (or mocking) in story
import Vuex from "vuex";
import { storiesOf } from '#storybook/vue';
import { withReadme } from 'storybook-readme';
import { withKnobs } from '#storybook/addon-knobs';
import HandoffMainView from './HandoffMainView.vue';
import readme from './README.md';
storiesOf('HandoffMainView', module)
.addDecorator(withReadme([readme]))
.addDecorator(withKnobs)
.add('Default', () => {
/* eslint-disable */
return {
components: { HandoffMainView },
data() {
return {
isLoading: true,
component: {
src: '',
data: [],
},
};
},
template: '<handoff-main-view :component="component" />',
store: new Vuex.Store({ // here
modules: {
namespaced: true,
actions: ...
}
}
};
});
If you are using Nuxt.js, here is how you can do it:
./storybook/store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const store = new Vuex.Store({
state: require("../store/index.js").state,
getters: require("../store/index.js").getters,
actions: require("../store/index.js").actions,
mutations: require("../store/index.js").mutations,
modules: {
ads: {
namespaced: true,
state: require("../store/ads.js").state,
getters: require("../store/ads.js").getters,
actions: require("../store/ads.js").actions,
mutations: require("../store/ads.js").mutations
},
features: {
namespaced: true,
state: require("../store/features.js").state,
getters: require("../store/features.js").getters,
actions: require("../store/features.js").actions,
mutations: require("../store/features.js").mutations
},
user: {
namespaced: true,
state: require("../store/user.js").state,
getters: require("../store/user.js").getters,
actions: require("../store/user.js").actions,
mutations: require("../store/user.js").mutations
},
}
});
export default store
Then in your story:
// ...
import store from '#/.storybook/store';
export default {
title: 'MyComponent'
};
export const MyComponentStory = () => ({
store: store,
// ...
})
You could try to use a decorator
import { createStore } from 'vuex';
const _vue = require("#storybook/vue3");
const _addons = require("#storybook/addons");
const withVueRouter = function withVueRouter() {
const store = arguments?.[0] || createStore({ state: {} });
return _addons.makeDecorator({
name: 'withStore',
parameterName: 'withStore',
wrapper: (storyFn, context) => {
_vue.app.use(store);
return storyFn(context);
}
});
};
export default withVueRouter;
usage
import withStore from '../../../config/storybook/decorators/withStore';
import { createStore } from 'vuex';
const store = createStore({
state: {
film: films[0],
},
});
export default {
title: 'film-details/FilmDetails',
decorators: [withStore(store)]
};
const FilmDetailsTemplate = (args) => ({
components: { FilmDetails },
template: '<FilmDetails/>',
});
export const template = FilmDetailsTemplate.bind({
});
If you're looking for a solution with .mdx type of story files, then you can mock the store behavior like this (I use the namespaced store configuration):
<!-- SomeComponent.stories.mdx -->
import Vuex from 'vuex';
[...]
export const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { SomeComponent },
store: new Vuex.Store({
modules: {
auth: {
namespaced: true,
state: {
user: {
id: 20,
avatar: "/images/avatar.png",
name: "John Doe",
login: "jonh.d",
}
},
getters: {
userPublicData: () => {
return {
id: 20,
avatar: "/images/avatar.png",
name: "John Doe",
login: "jonh.d",
};
},
}
},
},
}),
template: `
<SomeComponentv-bind="$props" />
`,
});
<Canvas>
<Story
name="Basic"
args={{
}}>
{Template.bind({})}
</Story>
</Canvas>
Im creating a simple spa todo app with vue + vuex.
My problem is that each module will have the same 5 default method for manipulating the state. If i decide to change the default state management behavior then i have to go to every module and update them. The five actions that all modules should have when written out in the module work, but as soon as i import the exact same object and assign it to the actions property on the module the action cant be found. and i get this error [vuex] unknown action type: namespacedTodos/getCollection
// This is in a component
mounted: function () {
this.$store.dispatch('namespacedTodos/getCollection')
},
// import baseActions from '../base-actions'
import baseGetters from '../base-getters'
import baseMutations from '../base-mutations'
import axios from 'axios/index'
import mainStore from '../index'
// import _ from 'lodash'
const namespacedTodos = {
namespaced: true,
state: {
collection: [],
defaultInstance: {},
collectionLoaded: false,
url: 'api/todos',
namespace: 'namespacedTodos'
},
mutations: baseMutations,
getters: baseGetters,
actions: {
getCollection: function ({state, commit}) {
if (state.collectionLoaded) {
return Promise.resolve({data: state.collection})
}
return axios.get(`${mainStore.state.baseUrl}/${state.url}`)
.then((response) => {
commit(`setCollection`, response.data.data)
return response
})
.catch((response) => {
console.log('Error Response: ', response)
throw response
})
}
},
strict: process.env.NODE_ENV !== 'production'
}
export default namespacedTodos
The above Code Works But the following Dose Not
import baseActions from '../base-actions'
import baseGetters from '../base-getters'
import baseMutations from '../base-mutations'
const namespacedTodos = {
namespaced: true,
state: {
collection: [],
defaultInstance: {},
collectionLoaded: false,
url: 'api/todos',
namespace: 'namespacedTodos'
},
mutations: baseMutations,
getters: baseGetters,
actions: baseActions,
strict: process.env.NODE_ENV !== 'production'
}
export default namespacedTodos
import axios from 'axios'
import _ from 'lodash'
import mainStore from './index'
export default {
getCollection: function ({state, commit}) {
if (state.collectionLoaded) {
return Promise.resolve({data: state.collection})
}
console.log('this: ', this)
console.log('Namespace: ', state.namespace)
return axios.get(`${mainStore.state.baseUrl}/${state.url}`)
.then((response) => {
commit(`setCollection`, response.data.data)
return response
})
.catch((response) => {
console.log('Error Response: ', response)
throw response
})
},
}
import baseActions from '../base-actions'
import baseGetters from '../base-getters'
import baseMutations from '../base-mutations'
const todos = {
namespaced: true,
state: {
collection: [],
defaultInstance: {},
collectionLoaded: false,
url: 'api/todos'
},
// The mutations get namespaced!!
mutations: Object.assign(baseMutations, {}),
// The getters get namespaced!!
getters: Object.assign(baseGetters, {}),
// The actions get namespaced!!
actions: Object.assign(baseActions, {
// any methods defined here will also be available
// You can over write existing methods when nessicary
}),
strict: process.env.NODE_ENV !== 'production'
}
export default todos