Skip to content

Commit

Permalink
docs(svelte-testing-library): add event, slot, binding, context examp…
Browse files Browse the repository at this point in the history
…les (#1366)
  • Loading branch information
mcous authored Feb 19, 2024
1 parent 2cd12ab commit 334201d
Showing 1 changed file with 250 additions and 28 deletions.
278 changes: 250 additions & 28 deletions docs/svelte-testing-library/example.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,275 @@ title: Example
sidebar_label: Example
---

## Component
For additional resources, patterns, and best practices about testing Svelte
components and other Svelte features, take a look at the [Svelte Society testing
recipes][testing-recipes].

```html
[testing-recipes]:
https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component

## 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`][user-event]
- Make assertions using `expect`, using matchers from
[`@testing-library/jest-dom`][jest-dom]

```html title="greeter.svelte"
<script>
export let name
let buttonText = 'Button'
let showGreeting = false
function handleClick() {
buttonText = 'Button Clicked'
}
const handleClick = () => (showGreeting = true)
</script>

<h1>Hello {name}!</h1>
<button on:click="{handleClick}">Greet</button>

<button on:click="{handleClick}">{buttonText}</button>
{#if showGreeting}
<p>Hello {name}</p>
{/if}
```

## Test
```js title="greeter.test.js"
import {render, screen} from '@testing-library/svelte'
import userEvent from '@testing-library/user-event'
import {expect, test} from 'vitest'

```js
// NOTE: jest-dom adds handy assertions to Jest (and Vitest) and it is recommended, but not required.
import '@testing-library/jest-dom'
import Greeter from './greeter.svelte'

import {render, fireEvent, screen} from '@testing-library/svelte'
test('no initial greeting', () => {
render(Greeter, {name: 'World'})

import Comp from '../Comp'
const button = screen.getByRole('button', {name: 'Greet'})
const greeting = screen.queryByText(/hello/iu)

test('shows proper heading when rendered', () => {
render(Comp, {name: 'World'})
const heading = screen.getByText('Hello World!')
expect(heading).toBeInTheDocument()
expect(button).toBeInTheDocument()
expect(greeting).not.toBeInTheDocument()
})

// Note: This is as an async test as we are using `fireEvent`
test('changes button text on click', async () => {
render(Comp, {name: 'World'})
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()
})
```

[user-event]: ../user-event/intro.mdx
[jest-dom]: https://github.com/testing-library/jest-dom

## Events

Events can be tested using spy functions. If you're using Vitest you can use
[`vi.fn()`][vi-fn] to create a spy.

:::info

Consider using function props to make testing events easier.

:::

```html title="button-with-event.svelte"
<button on:click>click me</button>
```

```html title="button-with-prop.svelte"
<script>
export let onClick
</script>

<button on:click="{onClick}">click me</button>
```

```js title="button.test.ts"
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()
})
```

[vi-fn]: https://vitest.dev/api/vi.html#vi-fn

## 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.

```html title="heading.svelte"
<h1>
<slot />
</h1>
```

```html title="heading.test.svelte"
<script>
import Heading from './heading.svelte'
</script>

<Heading>
<span data-testid="child" />
</Heading>
```

// Using await when firing events is unique to the svelte testing library because
// we have to wait for the next `tick` so that Svelte flushes all pending state changes.
await fireEvent.click(button)
```js title="heading.test.js"
import {render, screen, within} from '@testing-library/svelte'
import {expect, test} from 'vitest'

expect(button).toHaveTextContent('Button Clicked')
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()
})
```

For additional resources, patterns and best practices about testing svelte
components and other svelte features take a look at the
[Svelte Society testing recipe](https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component).
## 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.

```html title="text-input.svelte"
<script>
export let value = ''
</script>

<input type="text" bind:value="{value}" />
```

```html title="text-input.test.svelte"
<script>
import TextInput from './text-input.svelte'
export let valueStore
</script>

<TextInput bind:value="{$valueStore}" />
```

```js title="text-input.test.js"
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`][component-options] the component. When you use options like
`context`, be sure to place props under the `props` key.

[component-options]: ./api.mdx#component-options

```html title="notifications-provider.svelte"
<script>
import {setContext} from 'svelte'
import {writable} from 'svelte/stores'
setContext('messages', writable([]))
</script>
```

```html title="notifications.svelte"
<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>
```

```js title="notifications.test.js"
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')
})
```

0 comments on commit 334201d

Please sign in to comment.