Home > Software engineering >  VueJS test-utils can't find element inside child component
VueJS test-utils can't find element inside child component

Time:10-11

I'm trying to use findComponent with find method to find a child component's element and set it's value. But every time I run test, it gives me Cannot call setValue on an empty DOMWrapper. error.

Test file

import { mount } from '@vue/test-utils';
import Create from './Create.vue';
// import State from '@/components/State.vue';

describe('it tests Create component', () => {
    test('it emits create event and resets the form when form is valid and create button is clicked', async () => {
        const div = document.createElement('div');
        div.id = 'root';
        document.body.append(div);

        const expectedNameValue = 'TODO_NAME';
        const expectedStateValue = 'Pending';

        const wrapper = mount(Create, {
            attachTo: '#root',
        });

        await wrapper.find(`input`).setValue(expectedNameValue);
        await wrapper.findComponent({ ref: 'state-component' }).find('select').setValue(expectedStateValue);        
        
        await wrapper.find(`form`).trigger('submit');

        expect(wrapper.emitted().create).toBeTruthy();
        expect(wrapper.emitted().create[0]).toEqual([expectedNameValue]);
        expect(wrapper.emitted().create[1]).toEqual(['Pending']);
        expect(wrapper.find(`input[name='name']`).element.value).toEqual('');
        expect(wrapper.find(`input[name='state']`).element.value).toEqual('Pending');
    });
});

Create component

<template>
    <form @submit.prevent="createTodo" class="flex gap-2 w-full">
        <input class="flex-1 shadow rounded-md p-2 focus:ring-2 focus:ring-blue-900 focus:outline-none" type="text" placeholder="Todo Name" name="name" required/>
        <State ref="state-component"/>
        <button type="submit" class="rounded-md shadow text-white bg-blue-700 py-2 px-6">Create</button>
    </form>
</template>

<script>
import State from '@/components/State.vue';

export default {
    components: { State },
    emits: ['create'],
    methods: {
        createTodo(event) {
            const elems = event.target.elements;
            const todo = { name: elems.name.value, state: elems.state.value };
            this.$emit('create', todo);

            elems.name.value = '';
            elems.state.value = 'Pending';
        }
    }
}
</script>

<style scoped>
</style>

State component

<template>
  <select id="state-select" class="rounded-md bg-green-200 text-white" name="state">
    <option
      v-for="(state, index) in states"
      :selected="isSelected(state)"
      :key="index"
    >
      {{ state }}
    </option>
  </select>
</template>

<script>
export default {
  props: ["todo", "index"],
  data() {
    return {
      name: "",
      state: "",
      states: ["Pending", "In Progress", "Done"],
    };
  },
  created() {
    if(!this.todo) return true;

    this.state = this.todo.state;
    this.name = this.todo.name;
  },
  methods: {
    isSelected(equivalent){
      return equivalent === this.state;
    }
  }
};
</script>

<style scoped></style>

I'm fairly new to VueJS so I'm open to all tips and tricks, thanks.

CodePudding user response:

Some issues to fix:

  1. You don't need to attach the component to the document, so remove that:

    // ❌
    // const div = document.createElement('div');
    // div.id = 'root';
    // document.body.append(div);
    // const wrapper = mount(Create, { attachTo: '#root' });
    
    // ✅
    const wrapper = mount(Create);
    
  2. The template ref to the State component would be the component's root element, so no need to find() the <select>:

    // ❌
    // await wrapper.findComponent({ ref: 'state-component' }).find('select').setValue(expectedStateValue);
                                                              ^^^^^^^^^^^^^^^
    // ✅
    await wrapper.findComponent({ ref: 'state-component' }).setValue(expectedStateValue);
    
  3. The emitted() object key is the event name, and the value is an array of of arrays, containing emitted data. You can verify the first create-event data contains another object with toMatchObject(object):

    // ❌
    // expect(wrapper.emitted().create[0]).toEqual([expectedNameValue]);
    // expect(wrapper.emitted().create[1]).toEqual(['Pending']);
    
    // ✅
    expect(wrapper.emitted().create[0][0]).toMatchObject({ name: expectedNameValue, state: 'Pending' });
    
  4. The last assertion tries to find input[name='state'], but that's actually a <select>, not an <input>:

    // ❌
    // expect(wrapper.find(`input[name='state']`).element.value).toEqual('Pending')
                            ^^^^^
    // ✅
    expect(wrapper.find(`select[name='state']`).element.value).toEqual('Pending')
    

demo

  • Related