Testing Svelte context with component hierarchies
Daniel Irvine š³ļøāš
Posted on January 16, 2020
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:
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}
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>
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. MenuItem
s 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 ?
});
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();
});
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>
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);
});
});
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();
});
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
.
Posted on January 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 25, 2023