Example
For additional resources, patterns, and best practices about testing Svelte components and other Svelte features, take a look at the Svelte Society testing recipes.
Basic
This basic example demonstrates how to:
- Pass props to your Svelte component using
render - Query the structure of your component's DOM elements using
screen - Interact with your component using
@testing-library/user-event - Make assertions using
expect, using matchers from@testing-library/jest-dom
<script>
export let name
let showGreeting = false
const handleClick = () => (showGreeting = true)
</script>
<button on:click="{handleClick}">Greet</button>
{#if showGreeting}
<p>Hello {name}</p>
{/if}
import {render, screen} from '@testing-library/svelte'
import userEvent from '@testing-library/user-event'
import {expect, test} from 'vitest'
import Greeter from './greeter.svelte'
test('no initial greeting', () => {
render(Greeter, {name: 'World'})
const button = screen.getByRole('button', {name: 'Greet'})
const greeting = screen.queryByText(/hello/iu)
expect(button).toBeInTheDocument()
expect(greeting).not.toBeInTheDocument()
})
test('greeting appears on click', async () => {
const user = userEvent.setup()
render(Greeter, {name: 'World'})
const button = screen.getByRole('button')
await user.click(button)
const greeting = screen.getByText(/hello world/iu)
expect(greeting).toBeInTheDocument()
})
Events
Events can be tested using spy functions. If you're using Vitest you can use
vi.fn() to create a spy.
Consider using function props to make testing events easier.
<button on:click>click me</button>
<script>
export let onClick
</script>
<button on:click="{onClick}">click me</button>
import {render, screen} from '@testing-library/svelte'
import userEvent from '@testing-library/user-event'
import {expect, test, vi} from 'vitest'
import ButtonWithEvent from './button-with-event.svelte'
import ButtonWithProp from './button-with-prop.svelte'
test('button with event', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
const {component} = render(ButtonWithEvent)
component.$on('click', onClick)
const button = screen.getByRole('button')
await user.click(button)
expect(onClick).toHaveBeenCalledOnce()
})
test('button with function prop', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(ButtonWithProp, {onClick})
const button = screen.getByRole('button')
await user.click(button)
expect(onClick).toHaveBeenCalledOnce()
})
Slots
Slots cannot be tested directly. It's usually easier to structure your code so that you can test the user-facing results, leaving any slots as an implementation detail.
However, if slots are an important developer-facing API of your component, you can use a wrapper component and "dummy" children to test them. Test IDs can be helpful when testing slots in this manner.
<h1>
<slot />
</h1>
<script>
import Heading from './heading.svelte'
</script>
<Heading>
<span data-testid="child" />
</Heading>
import {render, screen, within} from '@testing-library/svelte'
import {expect, test} from 'vitest'
import HeadingTest from './heading.test.svelte'
test('heading with slot', () => {
render(HeadingTest)
const heading = screen.getByRole('heading')
const child = within(heading).getByTestId('child')
expect(child).toBeInTheDocument()
})
Two-way data binding
Two-way data binding cannot be tested directly. It's usually easier to structure your code so that you can test the user-facing results, leaving the binding as an implementation detail.
However, if two-way binding is an important developer-facing API of your component, you can use a wrapper component and writable store to test the binding itself.
<script>
export let value = ''
</script>
<input type="text" bind:value="{value}" />
<script>
import TextInput from './text-input.svelte'
export let valueStore
</script>
<TextInput bind:value="{$valueStore}" />
import {render, screen} from '@testing-library/svelte'
import userEvent from '@testing-library/user-event'
import {get, writable} from 'svelte/store'
import {expect, test} from 'vitest'
import TextInputTest from './text-input.test.svelte'
test('text input with value binding', async () => {
const user = userEvent.setup()
const valueStore = writable('')
render(TextInputTest, {valueStore})
const input = screen.getByRole('textbox')
await user.type(input, 'hello world')
expect(get(valueStore)).toBe('hello world')
})
Contexts
If your component requires access to contexts, you can pass those contexts in
when you render the component. When you use options like
context, be sure to place props under the props key.
<script>
import {setContext} from 'svelte'
import {writable} from 'svelte/stores'
setContext('messages', writable([]))
</script>
<script>
import {getContext} from 'svelte'
export let label
const messages = getContext('messages')
</script>
<div role="status" aria-label="{label}">
{#each $messages as message (message.id)}
<p>{message.text}</p>
<hr />
{/each}
</div>
import {render, screen} from '@testing-library/svelte'
import {readable} from 'svelte/store'
import {expect, test} from 'vitest'
import Notifications from './notifications.svelte'
test('notifications with messages from context', async () => {
const messages = readable([
{id: 'abc', text: 'hello'},
{id: 'def', text: 'world'},
])
render(Notifications, {
context: new Map([['messages', messages]]),
props: {label: 'Notifications'},
})
const status = screen.getByRole('status', {name: 'Notifications'})
expect(status).toHaveTextContent('hello world')
})