2

No doubt that Suspense feature leads to a cleaner code base, but as tidy as it is, it turns to be hard to test. Specifically it is not well documented yet.

case:

Regular app generated by VUE CLI

  • Tech stack: Vuex, Router, PWA, jest for unit testing

Challenge:

I made use of Suspense component as recommended as follows:

    <RouterView name="default" v-slot="{ Component, route }">
      <transition :name="route.meta.transition" mode="out-in" :duration="300" :key="route.path">
        <Suspense >
          <template #default>
            <component :is="Component" :key="route.path"/>
          </template>
          <template #fallback>
            <div class="top-0 right-0 h-screen w-screen z-50 flex justify-center items-center">
               <div class="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-yellow-700"></div>
            </div>
          </template>
        </Suspense>
      </transition>
    </RouterView>

I have few routes and views:

  • one of them is for Login view
  // here is the gotcha, if ever I removed async from setup the test runs well otherwise it always returns empty vm.
  async setup(){
    const router = useRouter()
    const route = useRoute()
    const form = reactive(new Form({
          username:'',
          password:'',
      }))
}

And my test suite as follows:


  test('Shows login form',  async () => {
      let wrapper = mount(Login,{
        // tried set global.stubs.transition to false
        renderDefaultSlot: true // tried as well to move this attr to beforeEach hook
      })
      expect(wrapper.exists()).toBe(true) // passes
      await nextTick()
      // tried to flushPromises()
      console.log(wrapper.vm) // always empty object {}
      expect(wrapper.findAll('div')).toBeTruthy() // fails accordingly as it can't run helper methods to get the parentElement
    })

Does any VUE veteran here can give a hint or workaround!

All open discussions on Github shows that I am not the only one stumbled on this issue, but for now it is just a discussion.

https://github.com/vuejs/vue-test-utils-next/issues/108#issue-611802592

https://github.com/vuejs/vue-test-utils/issues/956

Ahmed Shehab
  • 1,657
  • 15
  • 24

1 Answers1

9

After investigation wrote a little helper quoted from Github discussion above:

import {defineComponent,h,Suspense } from 'vue'
import { mount } from '@vue/test-utils'
import flushPromises from 'flush-promises';

const mountSuspense =  async (component, options) => {
    const wrapper = mount(defineComponent({
      render() {
        return h(Suspense, null, {
          default: h(component),
          fallback: h('div', 'fallback')
        })
      }}), ...options)

    await flushPromises()
    return wrapper
  }

  describe('App renderes', ()=>{
      test('About page renders',async()=>{
          const wrapper = await mountSuspense(About)
          await console.log(wrapper.text()) // it works
      })
  })
Ahmed Shehab
  • 1,657
  • 15
  • 24
  • Thanks that was very helpful, however this doesn't seem to that inclusive, for an instance, how can we test that the fallback gets redendered? – Mr.Toxy Apr 22 '21 at 11:36
  • @Mr.Toxy The instance exists now on the wrapper object as wrapper.text() or wrapper.html() - I advise you to console.log(wrapper) and mock async callback after awaiting flushPromises() or nextTick(). – Ahmed Shehab Apr 22 '21 at 17:03
  • 1
    I ended up finding https://github.com/vuejs/vue-test-utils-next/blob/master/tests/features/suspense.spec.ts which works flawlessly – Mr.Toxy Apr 23 '21 at 12:32
  • I'm trying to use this code but it fails on line: `const wrapper = mount(defineComponent({`. At `mount` saying `TypeError: Found non-callable @@iterator` any help? – Marc Pont Apr 04 '22 at 13:00
  • @MarcPont the fore-mentioned error seems to be dependency is missing or installation error, what test framework are you using. please see the updated VUE documentation is quite good. and as a quick heads up use vue-cli to generate your testing structure for you. cheers – Ahmed Shehab Apr 04 '22 at 19:11
  • But how to test/mock functions? Usually I can access them via `wrapper.vm.myFuncName`, but once the component is 'async" `wrapper.vm.myFuncName` is undefined – eXception Oct 25 '22 at 10:33
  • @eXception I believe you can import it as a property, read docs. https://v1.test-utils.vuejs.org/api/wrapper/#properties – Ahmed Shehab Oct 25 '22 at 18:30
  • @AhmedShehab I'm not sure what do you mean If you can provide a working example here, I'd be appreciated and accept your answer: https://stackoverflow.com/q/74192860/16280429 – eXception Oct 26 '22 at 07:18
  • 1
    @MarcPont the `TypeError: Found non-callable @@iterator` is happening if you pass any options as an object. Just remove the spread operator. – ontek Dec 08 '22 at 01:48
  • @AhmedShehab in your example, does the About component include its own suspense block? I am still wrapping my head around this feature, and I am unsure if I need to include suspense directly in the component, or if it is sufficient to include it in an upstream file. Ideally, I'd like to have a single fallback for all of the dynamic components in my layout, but I can't seem to get this to work like that. It works like this in my implementation, but in my test, I am only ever getting the fallback and the timeout I am testing with never resolves. – ontek Dec 08 '22 at 14:22
  • If anyone stumbles onto my previous question with a similar problem, the gist was that I needed to use vi.useFakeTimers because I was using a timeout to simulate an api request and I needed to stub those out. – ontek Dec 08 '22 at 17:26
  • @onetek for the first part, this link is a good start https://vuejs.org/guide/built-ins/suspense.html#combining-with-other-components , you can tweak it to your needs. Secondly if we need to mimic API call in a test we use mocks like in https://archive.jestjs.io/docs/ro/mock-functions – Ahmed Shehab Dec 08 '22 at 22:54