Testing Svelte async state changes

Daniel Irvine šŸ³ļøā€šŸŒˆ - Feb 28 '20 - - Dev Community

Hereā€™s a short Svelte component that displays the text Submitting... when a button is clicked:

<script>
  let submitting = false;

  const submit = async () => {
    submitting = true;
    await window.fetch('/foo');
    submitting = false;
  }
</script>

<button on:click="{submit}" />
{#if submitting}
  Submitting...
{/if}

Look carefully at the definition of submit. The submitting variable is set to true before the call to window.fetch and reset to false after the call returns.

The text is only rendered when submitting is true.

In other words, the Submitting... text appears after the button is clicked and disappears after the window.fetch call completes.

Why this is difficult to test

This behavior is tricky because one of our tests will need to get into the state where the Submitting... text is displayed, and freeze in that state while our test runs its expectations. To do that we need to use Svelteā€™s tick function to ensure the rendered output is updatetd.

Writing the tests

We require three unit tests!

  1. That the Submitting... text appears when the button is clicked.
  2. That initially, no text is displayed.
  3. That the Submitting... text disappears after the window.fetch call completes.

Testing the text appears

Letā€™s take a look at how weā€™d test this.

The test below uses my Svelte testing harness which is just a few dozen lines of code. Iā€™ve saved that at spec/svelteTestHarness.js, and this test exists as spec/Foo.spec.js.

For more information on how Iā€™m running these tests, take a look at my guide to Svelte unit testing.

import expect from "expect";
import Foo from "../src/Foo.svelte";
import { setDomDocument, mountComponent, click } from "./svelteTestHarness.js";
import { tick } from "svelte";

describe(Foo.name, () => {
  beforeEach(setDomDocument);

  beforeEach(() => {
    window.fetch = () => Promise.resolve({});
  });

  it("shows ā€˜Submitting...ā€™ when the button is clicked", async () => {
    mountComponent(Foo);

    click(container.querySelector("button"));
    await tick();

    expect(container.textContent).toContain("Submitting...");
  });
});

Notice the use of tick. Without that, this test wouldnā€™t pass. Thatā€™s because when our code executes submitting = true it doesnā€™t synchronously update the rendered output. Calling tick tells Svelte to go ahead and perform the update.

Crucially, we havenā€™t yet flushed the task queue: calling tick does not cause the fetch promise to execute.

In order to make that happen, we need to flush the task queue which weā€™ll do in the third test.

Testing initial state

First though we have to test the initial state. Without this test, we canā€™t prove that it was the button click that caused the text to appear: it could have been like that from the beginning.

it("initially isnā€™t showing the ā€˜Submittingā€™ text...", async () => {
  mountComponent(Foo);
  expect(container.textContent).not.toContain("Submitting...");
});

Testing the final state

Finally then, we check what happens after the promise resolves. We need to use await new Promise(setTimeout) to do this, which flushes the ask queue.

it("hides the ā€˜Submitting...ā€™ text when the request promise resolves", async () => {
  mountComponent(Foo);
  click(container.querySelector("button"));
  await new Promise(setTimeout);
  expect(container.textContent).not.toContain("Submitting...");
});

And there it is. Three tests to prove a small piece of behavior. Although it might seem overkill for such a small feature, these tests are quick to writeā€”that is, once you know how to write them šŸ¤£


Checkout out my guide to Svelte unit testing for more tips on how to test Svelte.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player