Search code examples
vue.jsjestjsvue-test-utils

Vue3: Testing load state fails in async function inside 'onMounted'


I'd like to test that a loading placeholder is displayed, while data from the server is fetching inside onMounted hook. Here is the component I'm testing:

<template>
  <section class="container mx-auto p-4">
    <div
      v-if="collections && Object.keys(collections).length"
      class="grid grid-cols-1 tablet:grid-cols-3 gap-4"
      data-test="collectionCards"
    >
      <div 
        v-for="collection in collections"
        :key="collection.cardSetId"
        class="relative shadow-lg rounded-sm flex flex-col justify-between"
      >
        <div class="flex justify-end p-4 border-b border-b-spacer">
          <p class="text-spacer uppercase text-sm">4 days ago</p>
        </div>
        
        <div class="flex flex-col justify-between h-full text-left">
          <div>
            <div class="card-cover"></div>
            <div class="text-black font-bold relative p-4">
              <h3 class="text-2xl" data-test="collectionName">{{ collection.name }}</h3>
              <p class="text-sm my-2">
                <span data-test="collectionLength">{{ collection.length }}</span> item(s)</p>
            </div>
            <div class="p-4">
              <base-collapse data-test="collectionDescription">{{ collection.description }}</base-collapse>
            </div>
          </div>

          <div class="flex justify-start p-4">
            <p class="text-sm">Author: <span class="font-bold">Kanstantsin Lazouski</span></p>
          </div>
        </div>

      </div>
    </div>
    <div v-else-if="isLoading" data-test="loadingCollections">Loading...</div>
    <div v-else class="h-screen flex" data-test="emptyCollectionWrapper">
      <EmptyCollection />
    </div>
  </section>
</template>

/*....*/
onMounted(async () => {
  isLoading.value = true;

  const collectionList = await getCollectionList(userId.value as string);

  try {
    collections.value = collectionList;
  } catch(err) {
    /* TODO: add more appropriate error handling */
    console.error(err);
  } finally {
    isLoading.value = false;
  }
})

But everything I tried was not successful. As far as I understand, we should firstly mount component and then call flushPromises in order to make Vue wait until promises inside onMounted will be resolved. So, between calling mount and flushPromises there should be a timespan when isLoading is set to true. The problem is that, when we log 'isLoading' inside the test, it will display true (which is correct), but actual HTML is different from what it is supposed to be (I receive EmptyCollection inside v-else). Here is my test that's not working though:

  it('renders "EmptyCollection" if "collectionList" is empty and "Loading..." while data is fetching', async () => {
    (getCollectionList as jest.Mock).mockResolvedValueOnce({});
    const wrapper = mount(MyCollections, wrapperOptions);


    expect(wrapper.find('[data-test="loadingCollections"]').exists()).toBe(true); // received: false
    await flushPromises();
    
    expect(wrapper.find('[data-test="collectionCards"]').exists()).toBe(false);
    expect(wrapper.find('[data-test="emptyCollectionWrapper"]').exists()).toBe(true);
  });

Thanks in advance.


Solution

  • const wrapper = mount(MyCollections, wrapperOptions);
    await wrapper.vm.$nextTick();
    expect(wrapper.find('[data-test="loadingCollections"]').exists()).toBe(true);
    await flushPromises();
    expect(wrapper.find('[data-test="loadingCollections"]').exists()).toBe(false);
    

    Hello! You need to wait until vue is updated the html. Then you need to wait until all promises are done, then you can check again. The reason for that is Vue updates html in ticks. At first your component mounts with default data. Then the onMounted hook is called, in which you're setting isLoading, which updates the DOM in the next tick. You need to await this DOM changes, then you can check the appearance of that element in your tests.

    Then you have to wait until api call is ended. This can be done by awaiting flushPromises (the side effect of what is that will include next tick of DOM update)