@@ -4,53 +4,275 @@ title: Example
44sidebar_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