Skip to content

Attempting to use vuex store fails with complaints about getters not being functions, even when they are #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
unikitty37 opened this issue Aug 17, 2020 · 6 comments
Labels
bug Something isn't working

Comments

@unikitty37
Copy link

unikitty37 commented Aug 17, 2020

Describe the bug A clear and concise description of what the bug is.

Attempting to use the vuex store in a test produces the error [vuex] getters should be function but "getters.user/isAnonymous" is true. when getters are defined on the store. The test itself is marked as failed without being executed.

The same component renders under vue-cli-service serve, with all behaviour as expected and no console errors.

isAnonymous is not called in any code path called by the test. If the getters are reordered, the error message will complain about whichever one is first.

There's nothing in the vuex example that covers this, and I seem to be doing the same stuff as in that example. Of course, that's the only example I can find for vuex, and it doesn't define any getters, so I can't tell if I've done something wrong or if this is a bug.

To Reproduce Steps to reproduce the behaviour:

Given a vue-cli project with the following test in tests/unit/components/helpers/test-component.spec.js:

import { render } from '@testing-library/vue'

import store from '@/store'
import testComponent from '@/components/test-component'

const renderComponent = (customStore) => render(testComponent, {
  store: { ...store, ...customStore },
})

describe('test-component.vue', () => {
  describe('when a user is logged out', () => {
    it('tells them their user level is insufficient', () => {
      const { container } = renderComponent({
        state: {
          user: {
            id: null,
            username: null,
            userLevel: 'anonymous',
          },
        },
      })

      expect(container).toHaveTextContent('You need to log in to see this content.')
    })
  })
})

with the following in src/store/index.js:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

import user from './user.js'

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    user,
  },
})

and the following in src/store/user.js:

const userLevels = {
  'anonymous': 0,
  'viewer': 1,
  'editor': 2,
  'admin': 3,
}

import { nullUser } from '@/objects/null-user'

const state = {
  ...nullUser,
}

const getters = {
  isAnonymous: (state) => userLevel(state.userLevel) === userLevels.anonymous,
  isViewer: (state) => userLevel(state.userLevel) === userLevels.viewer,
  isEditor: (state) => userLevel(state.userLevel) === userLevels.editor,
  isAdmin: (state) => userLevel(state.userLevel) === userLevels.admin,
}

const actions = {
  logIn ({ commit }, payload) {
    commit('LOG_IN', payload)
  },

  logOut ({ commit }) {
    commit('LOG_OUT')
  },
}

const mutations = {
  LOG_IN (state, payload) {
    state.id = payload.user.id
    state.username = payload.user.username
    state.userLevel = payload.user.userLevel
  },

  LOG_OUT (state) {
    state.id = nullUser.id
    state.username = nullUser.username
    state.userLevel = nullUser.userLevel
  },
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
}

then the test should run.

(The component under test merely renders one message if the store's user.id is null or undefined, and a different message otherwise; I've omitted it to save space.)

Expected behaviour

The test runs, and either passes or fails depending on whether the component is correctly written. But the component is actually rendered.

Related information:

  • @testing-library/vue version: 5.0.4
  • Vue version: 2.6.11
  • node version: 14.5.0
  • yarn version: 1.22.4

Additional context

@unikitty37 unikitty37 added the bug Something isn't working label Aug 17, 2020
@unikitty37
Copy link
Author

unikitty37 commented Aug 19, 2020

I seem to have fixed this, but using a different method of setting things up. This also stops Vue from emitting warnings about undefined custom components for router-link and the Buefy components.

If I use this in a helper file, and import render from there instead of from @testing-library/vue, routing and store work as expected:

import { render as originalRender } from '@testing-library/vue'
import { routes } from '@/router'
import { defaultStore } from '@/store'

import Buefy from 'buefy'

export const render = (ui, options, configCallback) => originalRender(ui, {
  ...options,
  routes,
  store: defaultStore(),
}, (vue, store, router) => {
  vue.use(Buefy)

  store.dispatch('user/logOut')

  if (configCallback && typeof configCallback === 'function') {
    return configCallback(vue, store, router)
  }
})

It is also necessary to change the exports in src/store/index.js so that defaultStore can be exported:

export const defaultStore = () => ({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    user,
  },
})

export default new Vuex.Store(defaultStore())

It's just a tiny bit confusing that the example code for vuex doesn't even mention that callback, with its store parameter — I only found it when I looked at the router example.

@MoSattler
Copy link

Hi @unikitty37, I have the same problem, may you elaborate how you solved this?

@afontcu
Copy link
Member

afontcu commented Sep 25, 2020

Hi @unikitty37, I have the same problem, may you elaborate how you solved this?

hi! looks like they provided a quite detailed solution for their problem. Would you like to share your snippets, so we can find what's wrong there?

It's just a tiny bit confusing that the example code for vuex doesn't even mention that callback, with its store parameter — I only found it when I looked at the router example.

That's true and worth adding!

@Atomic71
Copy link

I've just run into this annoyance myself, but what @unikitty37 is suggesting is not working for me.
In my situation, what fixes it is the following:

getters:{ getter: () => originalGetter }

After listening to the error, and making an already existing getter a function, it works flawlessly. However, this is completely different from what I have running in production. :/
getters: { getter: originalGetter }

Really am curious what's the issue here.

@goldengecko
Copy link

You should be able to pass in an already instantiated vuex store, and in render.js, it should recognise it as such by the instanceof Vuex.Store check, and use it as such.

Unfortunately this check seems to fail and it tries to instantiate a store by passing in the existing store as the arg to the new store.

I noticed the same sort of thing happening for the vue router later on in the same file.

This used to work fine for me when I was using webpack, jest and cjs in my app, but now I have changed to vite and modern js by default, and it no longer works. I'm not sure if there is something wrong about the way vite/vitest handles dependencies, but after a lot of hunting and thinking about it, I decided to try setting the vuex version in my app to what is defined in the testing library (v3.5.1) and ditto for vue-router (3.4.9).

This seemed to solve the problem.

You won't encounter the problem if you pass in a raw store config, but this appears to be a workaround if you want to pass in an already created store object.

If this rings any bells with anyone and you can expand on this explanation and/or suggest a solution that doesn't require downgrading my versions of vuex and vue-router, that would be much appreciated.

@goldengecko
Copy link

goldengecko commented Sep 12, 2024

OK, I have got to the bottom of this.

The cause of the problem is that in the render.js file in the testing library, it has the following code:

  if (store) {
    const Vuex = require('vuex')
    localVue.use(Vuex)

    vuexStore = store instanceof Vuex.Store ? store : new Vuex.Store(store)
  }

  if (routes) {
    const requiredRouter = require('vue-router')
    const VueRouter = requiredRouter.default || requiredRouter
    localVue.use(VueRouter)

    router = routes instanceof VueRouter ? routes : new VueRouter({routes})
  }

The package doesn't have its own version of vuex or vue-router, and doesn't even set them as peer dependencies, so it will get whatever is used in your main project.

In the latest versions of both the vuex and vue router vue 2 compatible versions, they have changed the packaging and vite loads the esm version of the packages, but since the vue testing library is using common js, it loads the common js version of vuex and vue router.

Since this code is different, the instanceof check fails.

To get around this, we need to ensure that the version of these libraries that we are loading in vitest is the common js version.

To make this happen, in your vitest.config.js file, within your defineConfig settings, add the following resolve alias settings:

      vuex: 'vuex/dist/vuex.common.js', // Force Vite to use the CommonJS version of Vuex
      'vue-router': 'vue-router/dist/vue-router.common.js', // Force Vite to use the CommonJS version of Vue Router

Now when you pass in an instantiated router or an instantiated vuex store, the instanceof check will work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants