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')
})