Search code examples
unit-testingvuejs3piniavitestvue-testing-library

Testing Pinia changes in a Vue 3 Component Unit Test


I have a component in my Vue 3 app which displays a checkbox. The checkbox can be manually checked by the user but it can also be checked/unchecked as a result of a Pinia state change. I'm pretty new to Unit Testing but I would assume that a good unit test for this component would include checking whether or not the checkbox reacts to the Pinia state correctly. However, in my Unit Test, when I change the Pinia state, the checkbox value does not change (the component itself works fine, it's only in the Unit Test that this does not work). Does anyone know what I am doing wrong?

As well as calling the store action to update the state I have also tried store.$patch and that doesn't work either.

This is my component:

<template>
  <div class="field-checkbox">
    <Checkbox role="checkbox" :aria-label="displayName" @change="checkGroupMember()" v-model="checked" :binary="true" />
    <label>{{displayName}}</label>
  </div>
</template>

<script setup lang="ts">
import { useContactBookStore } from "@/stores/contactBookStore";
import { ref, watch } from "vue";
import { storeToRefs } from "pinia";
const store = useContactBookStore();
const props = defineProps({
  groupMember: { type:Object, required: true }
});
const checked = ref(false);
const { getCheckedGroupMembers } = storeToRefs(store)
const displayName = ref(props.groupMember.title + " " + props.groupMember.firstName + " " + props.groupMember.lastName);

// set the initial value of the checkbox
updateCheckBox();

// watch the value of getCheckedGroupMembers in the store and if it
// changes re-evaluate the value of the checkbox
watch(getCheckedGroupMembers , () => {
  updateCheckBox();
},{ deep: true })

// when the checkbox is checked/unchecked, run the checkUser method
// in the store
function checkGroupMember() {
  const groupMember = {
    id:props.groupMember.id,
    title:props.groupMember.title,
    firstName:props.groupMember.firstName,
    lastName:props.groupMember.lastName
  }
  store.checkGroupMember(groupMember,checked.value);
}

// the checkbox is checked if the user is among the checked users
// in the store
function updateCheckBox() {
  const groupMember = {
    id: props.groupMember.id,
    title: props.groupMember.title,
    firstName: props.groupMember.firstName,
    lastName: props.groupMember.lastName
  }
  const exists = getCheckedGroupMembers.value.find((member) => member.id === groupMember.id)
  checked.value = !!exists;
}

</script>

and this is my Unit Test:

import { render, screen } from "@testing-library/vue";
import GroupMember from "@/components/ContactBook/GroupMember.vue";
import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
import { createTestingPinia } from "@pinia/testing";
import PrimeVue from "primevue/config";

import { createPinia, setActivePinia } from "pinia";
import Checkbox from 'primevue/checkbox';
import { useContactBookStore } from "@/stores/contactBookStore";

describe("GroupMember", () => {

  const mockUser:GroupMember = {id:"TT001",title:"Mr",firstName:"Ted",lastName:"Tester"}

  let mockProps = {groupMember:mockUser};

  render(GroupMember, {
    props: mockProps,

    global: {
      components: {Checkbox},
      plugins: [PrimeVue,
        createTestingPinia({
          initialState: {contactBook:{checkedGroupMembers:[mockUser]}},
          stubActions: false,
          createSpy: vi.fn,
          fakeApp:true
        }),
      ],
    },
  });

  setActivePinia(createPinia());

  it("Displays the user name in the correct format", async() => {
    const displayName = mockProps.groupMember.title + " " + mockProps.groupMember.firstName + " " + mockProps.groupMember.lastName;
    screen.getByText(displayName)
  });

  it("Shows the checkbox initially checked", async() => {
    let checkbox:any;
    const displayName = mockProps.groupMember.title + " " + mockProps.groupMember.firstName + " " + mockProps.groupMember.lastName;
    checkbox = screen.getAllByRole("checkbox", { name: displayName })[1]
    expect(checkbox.checked).toBe(true)
  });

  it("Should display the checkbox as unchecked when the store is updated", async() => {
    let checkbox:any;
    const displayName = mockProps.groupMember.title + " " + mockProps.groupMember.firstName + " " + mockProps.groupMember.lastName;
    checkbox = screen.getAllByRole("checkbox", { name: displayName })[1]
    const store = useContactBookStore();
    await store.checkGroupMember(mockUser,false);
    //await store.$patch({checkedGroupMembers:[]})  // this didn't work either
    expect(checkbox.checked).toBe(false)
  });
});

this is the error I get when the test runs:

54|     expect(checkbox.checked).toBe(false)
       |                             ^
     55|   });
     56| });

  - Expected   "false"
  + Received   "true"

Solution

  • When you render or mount a component in a unit test, there is no event loop running, so while you can change the state in the Pinia store, the component does not react to the alteration until "later": import { flushPromises } from '@vue/test-utils'; await flushPromises(), await component.vm.$nextTick or import { nextTick } from 'vue'; await nextTick().

    See https://v1.test-utils.vuejs.org/guides/testing-async-components.html where it is described that you can await triggers like clicking on the checkbox, but if the change happens out of band, you don't necessarily have enough awaiting going on.

    flushPromises is documented here