Testing Svelte context with component hierarchies

d_ir

Daniel Irvine šŸ³ļøā€šŸŒˆ

Posted on January 16, 2020

Testing Svelte context with component hierarchies

In the previous part of this series I looked at how to use mocking effectively when testing parent-child component relationships.

But that isnā€™t the only way of dealing with parent-child components, and component hierarchies in general. In this part Iā€™ll look at testing two components in the same test suite. So far Iā€™ve found this useful when dealing the Svelteā€™s context API.

All of the code samples in the post are available in this demo repo:

GitHub logo dirv / svelte-testing-demo

A demo repository for Svelte testing techniques

An example

Suppose you create a Menu component and a MenuItem component. Menu is responsible for opening and closing a list of items, and MenuItem represents a single item in that list. Crucially, itā€™s the MenuItemā€™s responsibility to close the Menu when it is selected.

Hereā€™s Menu. Iā€™ve simplified this by removing styles and by including only functionality thatā€™s relevant to this post.



<script context="module">
  export const key = {};
</script>

<script>
  import { setContext } from 'svelte';

  let open = false;

  const toggleMenu = () => open = !open;

  setContext(key, {
    toggleMenu
  });
</script>

<button on:click={toggleMenu} class="icon">Menu</button>
{#if open}
<div on:click={toggleMenu}>
  <slot />
</div>
{/if}


Enter fullscreen mode Exit fullscreen mode

And hereā€™s MenuItem (again, this is a simplified implementation).



<script>
  import { getContext, tick } from "svelte";
  import { key } from "./Menu.svelte";

  export let action;

  const { toggleMenu } = getContext(key);

  const closeMenuAndAct = async (event) => {
    event.stopPropagation();
    toggleMenu();
    await tick();
    action();
  };
</script>

<button on:click="{closeMenuAndAct}">
  <slot />
</button>


Enter fullscreen mode Exit fullscreen mode

Both of these components are coupled in two ways.

First, Menu uses <slot> to display all its children and itā€™s expected that some of these children will be instances of MenuItem.

Second, both components use the context API to share the toggleMenu function. MenuItems can communicate with the parent by invoking the toggleMenu function, which tells the Menu itā€™s time to close.

Could we programmatically call the context API to test Menu and MenuItem independently?

As far as I can tell, no we canā€™t. In order to do these weā€™d need to manipulate the context API. For example, for the MenuItem weā€™d need to make available a spy toggleMenu function that we could then assert on to check it was invoked.



it("invokes the toggleMenu context function", () => {
  // ? set up context here ?
});


Enter fullscreen mode Exit fullscreen mode

Trouble is, thereā€™s no supported way of calling the context API outside of components themselves. We could probably do it by using the component.$$ property in the way we did with bound values in the last part, but thatā€™s at risk of breaking in future.

Besides, these two components are meant to be used together, so why not test them together?

This is one place where React has Svelte beat!

Because React allows inline JSX, we could simply write a test like this:



const menuBox = () => container.querySelector(".overlay");

it("closes the menu when clicking the menuItem", () => {
  mount(<Menu><MenuItem /></Menu>);
  click(menuItem());
  expect(menuBox()).not.toBeNull();
});


Enter fullscreen mode Exit fullscreen mode

Unfortunately Svelte components must be defined in their own files, so we canā€™t do little inline hierarchies like this.

Ā Solution: define a test component for each test

In the test repo I have a directory spec/components where I keep little hierachies of components for specific tests. Sometimes the same test component can be used for multiple tests.

Hereā€™s spec/components/IsolatedMenuItem.svelte:



<script>
  import Menu from "../../src/Menu.svelte";
</script>

<Menu>
  <img alt="menu" slot="icon" src="menu.png" />
</Menu>


Enter fullscreen mode Exit fullscreen mode

There are a couple of tests I can write with this. First, the test that checks the menu is closed.

Hereā€™s spec/Menu.spec.js with just the first testā€”notice that I named the file after the parent component, but itā€™s testing both the parent and child.



import { tick } from "svelte";
import { mount, asSvelteComponent } from "./support/svelte.js";
import Menu from "../src/Menu.svelte";
import IsolatedMenuItem from "./components/IsolatedMenuItem.svelte";

const menuIcon = () => container.querySelector(".icon");
const menuBox = () => container.querySelector("div[class*=overlay]");

const click = async formElement => {
  const evt = document.createEvent("MouseEvents");
  evt.initEvent("click", true, true);
  formElement.dispatchEvent(evt);
  await tick();
  return evt;
};

describe(Menu.name, () => {
  asSvelteComponent();

  it("closes the menu when a menu item is selected", async () => {
    mount(IsolatedMenuItem);
    await click(menuIcon());
    await click(menuBox().querySelector("button"));
    expect(menuBox()).toBe(null);
  });
});


Enter fullscreen mode Exit fullscreen mode

Notice how similar this is to the React version above. The difference is just that the component exists within its own file instead of being written inline.

(By the way, I think this is the first time in the series that Iā€™ve shown any DOM events... click is something that could be cleaned up a little. Weā€™ll look at that in the next post!)

The second test uses the spy prop of IsolatedMenuItem.



it("performs action when menu item chosen", async () => {
  const action = jasmine.createSpy();
  mount(IsolatedMenuItem, { spy: action });
  await click(menuIcon());
  await click(menuBox().querySelector("button"));
  expect(action).toHaveBeenCalled();
});


Enter fullscreen mode Exit fullscreen mode

For this test component I named the prop spy, which is used to set the action prop on MenuItem. Perhaps I should have kept its name as action. The benefit of naming it spy is that itā€™s clear what its purpose is for. But I'm still undecided if thatā€™s a benefit or not.

Use with svelte-routing

Iā€™ve also used this with svelte-routing when I defined my own wrapped version of Route. These classes also use the context API so itā€™s similar to the example shown above.

In the next (and final!) post in this series, weā€™ll look at raising events like the one we saw here, the click event, and how we can test components together with more complex browser APIs like setTimeout.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
d_ir

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related