Skip to content

Commit 334201d

Browse files
authored
docs(svelte-testing-library): add event, slot, binding, context examples (#1366)
1 parent 2cd12ab commit 334201d

File tree

1 file changed

+250
-28
lines changed

1 file changed

+250
-28
lines changed

docs/svelte-testing-library/example.mdx

+250-28
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,275 @@ title: Example
44
sidebar_label: Example
55
---
66

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

9-
```html
11+
[testing-recipes]:
12+
https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component
13+
14+
## Basic
15+
16+
This basic example demonstrates how to:
17+
18+
- Pass props to your Svelte component using `render`
19+
- Query the structure of your component's DOM elements using `screen`
20+
- Interact with your component using [`@testing-library/user-event`][user-event]
21+
- Make assertions using `expect`, using matchers from
22+
[`@testing-library/jest-dom`][jest-dom]
23+
24+
```html title="greeter.svelte"
1025
<script>
1126
export let name
1227
13-
let buttonText = 'Button'
28+
let showGreeting = false
1429
15-
function handleClick() {
16-
buttonText = 'Button Clicked'
17-
}
30+
const handleClick = () => (showGreeting = true)
1831
</script>
1932

20-
<h1>Hello {name}!</h1>
33+
<button on:click="{handleClick}">Greet</button>
2134

22-
<button on:click="{handleClick}">{buttonText}</button>
35+
{#if showGreeting}
36+
<p>Hello {name}</p>
37+
{/if}
2338
```
2439

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

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

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

33-
import Comp from '../Comp'
50+
const button = screen.getByRole('button', {name: 'Greet'})
51+
const greeting = screen.queryByText(/hello/iu)
3452

35-
test('shows proper heading when rendered', () => {
36-
render(Comp, {name: 'World'})
37-
const heading = screen.getByText('Hello World!')
38-
expect(heading).toBeInTheDocument()
53+
expect(button).toBeInTheDocument()
54+
expect(greeting).not.toBeInTheDocument()
3955
})
4056

41-
// Note: This is as an async test as we are using `fireEvent`
42-
test('changes button text on click', async () => {
43-
render(Comp, {name: 'World'})
57+
test('greeting appears on click', async () => {
58+
const user = userEvent.setup()
59+
render(Greeter, {name: 'World'})
60+
4461
const button = screen.getByRole('button')
62+
await user.click(button)
63+
const greeting = screen.getByText(/hello world/iu)
64+
65+
expect(greeting).toBeInTheDocument()
66+
})
67+
```
68+
69+
[user-event]: ../user-event/intro.mdx
70+
[jest-dom]: https://github.com/testing-library/jest-dom
71+
72+
## Events
73+
74+
Events can be tested using spy functions. If you're using Vitest you can use
75+
[`vi.fn()`][vi-fn] to create a spy.
76+
77+
:::info
78+
79+
Consider using function props to make testing events easier.
80+
81+
:::
82+
83+
```html title="button-with-event.svelte"
84+
<button on:click>click me</button>
85+
```
86+
87+
```html title="button-with-prop.svelte"
88+
<script>
89+
export let onClick
90+
</script>
91+
92+
<button on:click="{onClick}">click me</button>
93+
```
94+
95+
```js title="button.test.ts"
96+
import {render, screen} from '@testing-library/svelte'
97+
import userEvent from '@testing-library/user-event'
98+
import {expect, test, vi} from 'vitest'
99+
100+
import ButtonWithEvent from './button-with-event.svelte'
101+
import ButtonWithProp from './button-with-prop.svelte'
102+
103+
test('button with event', async () => {
104+
const user = userEvent.setup()
105+
const onClick = vi.fn()
106+
107+
const {component} = render(ButtonWithEvent)
108+
component.$on('click', onClick)
109+
110+
const button = screen.getByRole('button')
111+
await user.click(button)
112+
113+
expect(onClick).toHaveBeenCalledOnce()
114+
})
115+
116+
test('button with function prop', async () => {
117+
const user = userEvent.setup()
118+
const onClick = vi.fn()
119+
120+
render(ButtonWithProp, {onClick})
121+
122+
const button = screen.getByRole('button')
123+
await user.click(button)
124+
125+
expect(onClick).toHaveBeenCalledOnce()
126+
})
127+
```
128+
129+
[vi-fn]: https://vitest.dev/api/vi.html#vi-fn
130+
131+
## Slots
132+
133+
Slots cannot be tested directly. It's usually easier to structure your code so
134+
that you can test the user-facing results, leaving any slots as an
135+
implementation detail.
136+
137+
However, if slots are an important developer-facing API of your component, you
138+
can use a wrapper component and "dummy" children to test them. Test IDs can be
139+
helpful when testing slots in this manner.
140+
141+
```html title="heading.svelte"
142+
<h1>
143+
<slot />
144+
</h1>
145+
```
146+
147+
```html title="heading.test.svelte"
148+
<script>
149+
import Heading from './heading.svelte'
150+
</script>
151+
152+
<Heading>
153+
<span data-testid="child" />
154+
</Heading>
155+
```
45156

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

50-
expect(button).toHaveTextContent('Button Clicked')
161+
import HeadingTest from './heading.test.svelte'
162+
163+
test('heading with slot', () => {
164+
render(HeadingTest)
165+
166+
const heading = screen.getByRole('heading')
167+
const child = within(heading).getByTestId('child')
168+
169+
expect(child).toBeInTheDocument()
51170
})
52171
```
53172

54-
For additional resources, patterns and best practices about testing svelte
55-
components and other svelte features take a look at the
56-
[Svelte Society testing recipe](https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component).
173+
## Two-way data binding
174+
175+
Two-way data binding cannot be tested directly. It's usually easier to structure
176+
your code so that you can test the user-facing results, leaving the binding as
177+
an implementation detail.
178+
179+
However, if two-way binding is an important developer-facing API of your
180+
component, you can use a wrapper component and writable store to test the
181+
binding itself.
182+
183+
```html title="text-input.svelte"
184+
<script>
185+
export let value = ''
186+
</script>
187+
188+
<input type="text" bind:value="{value}" />
189+
```
190+
191+
```html title="text-input.test.svelte"
192+
<script>
193+
import TextInput from './text-input.svelte'
194+
195+
export let valueStore
196+
</script>
197+
198+
<TextInput bind:value="{$valueStore}" />
199+
```
200+
201+
```js title="text-input.test.js"
202+
import {render, screen} from '@testing-library/svelte'
203+
import userEvent from '@testing-library/user-event'
204+
import {get, writable} from 'svelte/store'
205+
import {expect, test} from 'vitest'
206+
207+
import TextInputTest from './text-input.test.svelte'
208+
209+
test('text input with value binding', async () => {
210+
const user = userEvent.setup()
211+
const valueStore = writable('')
212+
213+
render(TextInputTest, {valueStore})
214+
215+
const input = screen.getByRole('textbox')
216+
await user.type(input, 'hello world')
217+
218+
expect(get(valueStore)).toBe('hello world')
219+
})
220+
```
221+
222+
## Contexts
223+
224+
If your component requires access to contexts, you can pass those contexts in
225+
when you [`render`][component-options] the component. When you use options like
226+
`context`, be sure to place props under the `props` key.
227+
228+
[component-options]: ./api.mdx#component-options
229+
230+
```html title="notifications-provider.svelte"
231+
<script>
232+
import {setContext} from 'svelte'
233+
import {writable} from 'svelte/stores'
234+
235+
setContext('messages', writable([]))
236+
</script>
237+
```
238+
239+
```html title="notifications.svelte"
240+
<script>
241+
import {getContext} from 'svelte'
242+
243+
export let label
244+
245+
const messages = getContext('messages')
246+
</script>
247+
248+
<div role="status" aria-label="{label}">
249+
{#each $messages as message (message.id)}
250+
<p>{message.text}</p>
251+
<hr />
252+
{/each}
253+
</div>
254+
```
255+
256+
```js title="notifications.test.js"
257+
import {render, screen} from '@testing-library/svelte'
258+
import {readable} from 'svelte/store'
259+
import {expect, test} from 'vitest'
260+
261+
import Notifications from './notifications.svelte'
262+
263+
test('notifications with messages from context', async () => {
264+
const messages = readable([
265+
{id: 'abc', text: 'hello'},
266+
{id: 'def', text: 'world'},
267+
])
268+
269+
render(Notifications, {
270+
context: new Map([['messages', messages]]),
271+
props: {label: 'Notifications'},
272+
})
273+
274+
const status = screen.getByRole('status', {name: 'Notifications'})
275+
276+
expect(status).toHaveTextContent('hello world')
277+
})
278+
```

0 commit comments

Comments
 (0)