diff --git a/docs/svelte-testing-library/example.mdx b/docs/svelte-testing-library/example.mdx index 26c8fe34a..639e55d81 100644 --- a/docs/svelte-testing-library/example.mdx +++ b/docs/svelte-testing-library/example.mdx @@ -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" -

Hello {name}!

+ - +{#if showGreeting} +

Hello {name}

+{/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" + +``` + +```html title="button-with-prop.svelte" + + + +``` + +```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" +

+ +

+``` + +```html title="heading.test.svelte" + + + + + +``` - // 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" + + + +``` + +```html title="text-input.test.svelte" + + + +``` + +```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" + +``` + +```html title="notifications.svelte" + + +
+ {#each $messages as message (message.id)} +

{message.text}

+
+ {/each} +
+``` + +```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') +}) +```