Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(svelte-testing-library): add event, slot, binding, context examples #1366

Merged
merged 5 commits into from
Feb 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
})
```