Chapter 7 – Testing Vue.js Components with Jest

Chapter 7

Test Vue.js Slots

Slots are a means of making content distribution happen in the world of web components. Vue.js slots are made in accordance with the Web Component specs (https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md), meaning that if you learn how to use them in Vue.js, they will be useful moving forward.

They make the structures of components much more flexible, moving the responsibility of managing state to the parent component. For example, we can have a List component, and different kinds of item components, such as ListItem and ListItemImage. These will be used as follows:

<template>

  <List>

    <ListItem :someProp="someValue" />

    <ListItem :someProp="someValue" />

    <ListItemImage :image="imageUrl" :someProp="someValue" />

  </List>

</template>

The inner content of List is the slot itself, and it is accessible via the <slot> tag. Hence, the List implementation appears as follows:

<template>

  <ul>

    <!-- slot here will equal to what's inside <List> -->

    <slot></slot>

  </ul>

</template>

Now, let's say that the ListItem component looks like this:

<template>

  <li> {{ someProp }} </li>

</template>

Then, the final result rendered by Vue.js would be:

<ul>

  <li> someValue </li>

  <li> someValue </li>

  <li> someValue </li> <!-- assume the same implementation for ListItemImage -->

</ul>

Making MessageList Slot-Based

Now, let's take a look at the MessageList.vue component:

<template>

  <ul>

    <Message

      @message-clicked="handleMessageClick"

      :message="message"

      v-for="message in messages"

      :key="message"/>

  </ul>

</template>

MessageList has hardcoded the Message component inside. In one way, that is more automated but, in another, it is lacking in any flexibility whatsoever. What if you want to have different types of Message components? What about changing their structure or styling? That's where slots come in handy.

Let's now change Message.vue to use slots. First, move the <Message... part to the App.vue component, along with the handleMessageClick method, so that it's used externally:

<template>

  <div id="app">

    <MessageList>

      <Message

        @message-clicked="handleMessageClick"

        :message="message"

        v-for="message in messages"

        :key="message"/>

    </MessageList>

  </div>

</template>

<script>

import MessageList from "./components/MessageList";

import Message from "./components/Message";

export default {

  name: "app",

  data: () => ({ messages: ["Hey John", "Howdy Paco"] }),

  methods: {

    handleMessageClick(message) {

      console.log(message);

    }

  },

  components: {

    MessageList,

    Message

  }

};

</script>

Don't forget to import the Message component and add it to the components option in App.vue.

Then, in MessageList.vue, we can remove the references to Message. This now appears as follows:

<template>

  <ul class="list-messages">

    <slot></slot>

  </ul>

</template>

<script>

export default {

  name: "MessageList"

};

</script>

$children and $slots

Vue components have two instance variables that are useful for accessing slots:

  • $children: An array of Vue component instances of the default slot
  • $slots: An object of VNodes mapping all the slots defined in the component instance

The $slots object has more data available. In fact, $children is just a portion of the $slots variable that could be accessed the same way by mapping over the $slots.default array, filtered by Vue component instances:

const children = this.$slots.default

  .map(vnode => vnode.componentInstance)

  .filter(cmp => !!cmp);

Testing Slots

The aspect of slots that we probably want to test the most is where they end up in the component, and for that, we can reuse the skills we learned in Chapter 3, Test Styles and Structure of Vue.js Components in Jest.

Right now, most of the tests in MessageList.test.js will fail, so let's remove them all (or comment them out), and focus on slot testing.

One thing we can test is to make sure that the Message components end up within a ul element with the list-messages class. In order to pass slots to the MessageList component, we can use the slots property of the options object of the mount or shallowMount methods. So, let's create a beforeEach method (https://jestjs.io/docs/en/api.html#beforeeachfn-timeout) with the following code:

beforeEach(() => {

  cmp = shallowMount(MessageList, {

    slots: {

      default: '<div class="fake-msg"></div>'

    }

  });

});

Since we just want to test whether the messages are rendered, we can search for <div class="fake-msg"></div> as follows:

it("Messages are inserted in a ul.list-messages element", () => {

  const list = cmp.find("ul.list-messages");

  expect(list.findAll(".fake-msg").length).toBe(1);

});

And that should be good to go. The slots option also accepts a component declaration, and even an array, so we could write the following:

import AnyComponent from "anycomponent";

shallowMount(MessageList, {

  slots: {

    default: AnyComponent // or [AnyComponent, AnyComponent]

  }

});

The problem with this is that it is very limited; you cannot override props for example, and we need that for the Message component since it has a required property. This should affect the cases that you really need to test slots with the expected components; for example, if you want to make sure that MessageList expects only Message components as slots. That's on track and, at some point, it will land in vue-test-utils (https://github.com/vuejs/vue-test-utils/issues/41#issue-255235880).

As a workaround, we can accomplish that by using a render function (https://vuejs.org/v2/guide/render-function.html). Consequently, we can rewrite the test to be more specific:

beforeEach(() => {

  const messageWrapper = {

    render(h) {

      return h(Message, { props: { message: "hey" } });

    }

  };

  cmp = shallowMount(MessageList, {

    slots: {

      default: messageWrapper

    }

  });

});

it("Messages are inserted in a MessageList component", () => {

  const list = cmp.find(MessageList);

  expect(list.find(Message).isVueInstance()).toBe(true);

});

Testing Named Slots

The unnamed slot we used previously is called the default slot, but we can have multiple slots by using named slots. Let's now add a header to the MessageList.vue component:

<template>

  <div>

    <header class="list-header">

      <slot name="header">

        This is a default header

      </slot>

    </header>

    <ul class="list-messages">

      <slot></slot>

    </ul>

  </div>

</template>

By using <slot name="header">, we're defining another slot for the header. You can see the This is a default header text inside the slot. This is displayed as the default content when a slot is not passed to the component, and that's applicable to the default slot.

Then, from App.vue, we can add a header to the MessageList component by using the slot="header" attribute:

<template>

  <div id="app">

    <MessageList>

      <header slot="header">

        Awesome header

      </header>

      <Message

        @message-clicked="handleMessageClick"

        :message="message"

        v-for="message in messages"

        :key="message"/>

    </MessageList>

  </div>

</template>

It's now time to write a unit test for it. Testing named slots is just like testing a default slot; the same dynamics apply. So, we can start by verifying that the header slot is rendered within the <header class="list-header"> element, and that it renders default text when no header slot is passed by. In MessageList.test.js, we have the following:

it("Header slot renders a default header text", () => {

  const header = cmp.find(".list-header");

  expect(header.text().trim()).toBe("This is a default header");

});

Then, the same but checking the default content gets replaced when we mock the header slot:

it("Header slot is rendered withing .list-header", () => {

  const component = shallowMount(MessageList, {

    slots: {

      header: "<div>What an awesome header</div>"

    }

  });

  const header = component.find(".list-header");

  expect(header.text().trim()).toBe("What an awesome header");

});

We can see that the header slot used in this last test is wrapped in a <div>. It's important that slots are wrapped in an HTML tag, otherwise vue-test-utils will complain.

Testing Contextual Slot Specs

We have tested how and where the slots render, and that's probably the most important aspect. However, it doesn't end there. If you pass component instances as slots, just as we're doing in the default slot with Message, you can test the functionality related to them.

Be careful as to what you test here. This is probably something you don't need to do in most cases since the functional tests of a component should belong to that component test. When talking about testing the functionality of slots, we test how a slot must behave in the context of the component where that slot is used, and this is something that is not very common. Normally, we just pass the slot and forget about it. So, don't get too attached to the following example – its sole purpose is to demonstrate how the tool works.

Let's say that, for whatever reason, in the context of the MessageList component, all the Message components must have a length of higher than 5. We can test this as follows:

it("Message length is higher than 5", () => {

  const messages = cmp.findAll(Message);

  messages.wrappers.forEach(c => {

    expect(c.vm.message.length).toBeGreaterThan(5);

  });

});

findAll returns an object containing an array of wrappers where we can access its vm component instance property. This test will fail because the message has a length of 3, so go to the beforeEach function and make it longer:

beforeEach(() => {

  const messageWrapper = {

    render(h) {

      return h(Message, { props: { message: "hey yo" } });

    }

  };

});

Then, it should pass.

Wrapping Up

Testing slots is quite simple. Normally, we'd like to test that they're placed and rendered as we want, so it is just like testing style and structure, knowing how slots behave or can be mocked. You won't need to test slot functionality very often in all probability.

Keep in mind that you should only test things related to slots when you want to test slots and think twice about whether what you're testing belongs to the slot test or the component test itself.

You can find the code relating to this chapter on GitHub (https://github.com/alexjoverm/vue-testing-series/tree/test-slots).