One of the great things about Svelte is its compiler, producing a minimal runtime and providing a great, declarative way to build components. Unfortunately, unlike many other JavaScript frameworks, which use jsx or tagged template functions to render components in a declarative manner inside JavaScript files, Svelte has no such feature. Consider the following structure of composed components:
<Popover>
<PopoverTrigger>Open Menu</PopoverTrigger>
<PopoverMenu>
<PopoverMenuItem href="/account">Account</PopoverMenuItem>
<PopoverMenuItem href="/logout">Log out</PopoverMenuItem>
</PopoverMenu>
</Popover>
There are existing ways to render this in your tests, but none of them are ergonomic. You can:
.svelte file for each configuration of your composed
components you want to test and pass that component to render(); or.svelte file and wrap all of your configurations in
{#snippet <name>()}...{/snippet}, along with a {@render children()}. You
export all the snippets and import them along with the default export, pass
the default export to render(...) and the snippet to the children
property; orUsing this library, you can write your tests in Svelte syntax. This gives you much greater ergonomics and flexibility, especially for the above composition. Your tests can look like this:
<!-- Popover.test.svelte -->
<script>
import { Test, Check } from 'svelte-declarative-testing/vitest-browser-svelte';
import { Popover, PopoverTrigger, PopoverMenu, PopoverMenuItem } from './';
</script>
<Test it="opens the menu when the trigger is clicked">
<!-- Anything passed via the mount() snippet is rendered for each test -->
{#snippet mount()}
<Popover>
<PopoverTrigger>Open Menu</PopoverTrigger>
<PopoverMenu>
<PopoverMenuItem href="/account">Account</PopoverMenuItem>
<PopoverMenuItem href="/logout">Log out</PopoverMenuItem>
</PopoverMenu>
</Popover>
{/snippet}
<!-- one or more test functions can be provided as checks -->
<Check
fn={async (screen) => {
expect(screen.getByRole('menu', { includeHidden: true })).not.toBeVisible();
await screen.getByRole('button', { name: 'Open Menu' }).click();
expect(screen.getByRole('menu')).toBeVisible();
}}
/>
</Test>
Install as part of your project's devDependencies through your preferred
package manager.
npm install --save-dev svelte-declarative-testing
There are 2 ways to activate your declarative svelte test files, using the provided Vite plugins or by importing them directly into another test file to mount them.
In order to work with the VSCode Vitest extension, 2 plugins are provided to instrument the source code. You will also need to make sure Vitest includes your test files:
import svelteDeclarativeTesting from 'svelte-declarative-testing/vitest';
export default defineConfig({
plugins: [
// ...
...svelteDeclarativeTesting(),
],
// ...
test: {
include: [
'src/**/*.svelte.{test,spec}.{js,ts}',
// Includes .test.svelte and .spec.svelte
'src/**/*.{test,spec}.svelte',
],
},
});
[!WARNING] This approach will not allow you to trigger test runs using the Vitest extension for VSCode. There may be some other side effects also.
If you don't want to use the plugins, simply import your Svelte-syntax test files into your existing unit tests:
// Popover.test.js
import PopoverIntegrationTests from './Popover.test.svelte';
import { render } from '@testing-library/svelte'; // or vitest-browser-svelte
describe('Unit tests', () => {
// ... existing unit tests
});
describe('Integration tests', () => {
render(PopoverIntegrationTests);
});
Writing tests is quite intuitive. It is a similar structure to JavaScript test
files, with describe() and test() being replaced by <Describe ...> and
<Test ...> respectively. Additionally, there is a <Check> function that
allows you to provide the test functions.
Convenience wrappers are provided for @testing-library/svelte and vitest-browser-svelte.
import { Describe, Test, Check } from 'svelte-declarative-testing/testing-library';
import { Describe, Test, Check } from 'svelte-declarative-testing/vitest-browser-svelte';
Alternatively, you can import the core functions and provide a custom render()
function to the Test component.
import { Describe, Test, Check } from 'svelte-declarative-testing';
<Describe ...>Wraps your tests in a suite, similare to describe().
<Describe label="My Integration Tests">
Inside Describe, tests go in a tests() snippet. Any other children will be
rendered for each test.
| Properties | Type | Description |
|---|---|---|
label |
string |
The descriptive label for the test suite |
mount |
Snippet |
Any component(s) that you wish to render for the tests |
children |
Snippet |
A snippet block containing one or more <Test> elements |
skip |
any |
Skips the suite if truthy, just like describe.skip() |
only |
any |
Only run this suite if truthy, just like describe.only() |
todo |
any |
Mark the suite as todo if truthy, just like describe.todo() |
shuffle |
any |
Mark the suite as todo if truthy, just like describe.todo() |
skipIf |
function |
Skips the suite if the function returns a truthy value, just like describe.skipIf() |
runIf |
function |
Runs the suite if the function returns a truthy value, just like describe.runIf() |
<Describe label="My Integration Tests">
{#snippet mount()}
<Button>My button</Button>
{/snippet}
<Test it="renders an accessible button">
<!-- Test body goes here -->
</Test>
<Test it="does something when I click the button">
<!-- Test body goes here -->
</Test>
</Describe>
<Test ...>Describes a test.
<Test it="does what I expect it to do">
Inside Test, checks go in a checks() snippet. Any other children will be
rendered for each test. If no other children are provided, the children of the
parent <Describe> are rendered.
| Properties | Type | Description |
|---|---|---|
it |
string |
The description of the test |
mount |
Snippet |
Any component(s) that you wish to render for the test |
children |
Snippet |
A snippet block containing one or more <Check> elements |
skip |
any |
Skips the test if truthy, just like test.skip() |
only |
any |
Only run this test if truthy, just like test.only() |
todo |
any |
Mark the test as todo if truthy, just like test.todo() |
fails |
any |
Passes the test on failure if truthy, just like test.fails() |
skipIf |
function |
Skips the test if the function returns a truthy value, just like test.skipIf() |
runIf |
function |
Runs the test if the function returns a truthy value, just like test.runIf() |
render() |
function |
Provides a custom render function (not available on the wrapper components` |
<Test it="renders an accessible button">
<Button>My button</Button>
{#snippet checks()}
<Check fn={ /* ... */ } />
<Check fn={ /* ... */ } />
{/snippet}
</Describe>
<Check ...>Provides a test function.
<Check fn={() => expect(1 + 1).toBe(2)} />
You can provide as many checks as you like and they will run sequentially.
| Properties | Type | Description |
|---|---|---|
fn |
function |
A function to be executed as part of the test body |
<Test it="renders an accessible button">
<Button>My button</Button>
{#snippet checks()}
<Check fn={({queryByRole}) => {
expect(queryByRole("button")).toBeIntheDocument();
}}/>
{/snippet}
</Describe>
This is not a new idea and was originally posited by @7nik and @paoloricciuti on the Svelte bug tracker. Thanks also to @sheremet-va, who assisted with the Vitest test detection plugins.