From 7fc39e5cf713f412b14640752d249c1382fe9f54 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Mon, 29 Jan 2024 18:43:24 +1100 Subject: [PATCH 1/5] docs(svelte-testing-library): add event, slot, binding, and context examples --- docs/svelte-testing-library/example.mdx | 282 +++++++++++++++++++++--- 1 file changed, 254 insertions(+), 28 deletions(-) diff --git a/docs/svelte-testing-library/example.mdx b/docs/svelte-testing-library/example.mdx index 26c8fe34a..98870f87b 100644 --- a/docs/svelte-testing-library/example.mdx +++ b/docs/svelte-testing-library/example.mdx @@ -4,53 +4,279 @@ title: Example sidebar_label: Example --- -## Component +- [Basic](#basic) +- [Events](#events) +- [Slots](#slots) +- [Two-way data binding](#two-way-data-binding) +- [Contexts](#contexts) -```html +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]. + +[testing-recipes]: + https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component + +## Basic + +### Component + +```html filename="src/greeter.svelte" -

Hello {name}!

+ - +{#if showGreeting} +

Hello {name}

+{/if} ``` -## Test +### Tests -```js -// NOTE: jest-dom adds handy assertions to Jest (and Vitest) and it is recommended, but not required. -import '@testing-library/jest-dom' +```js filename="src/__tests__/greeter.test.js" +import {test} from 'vitest' -import {render, fireEvent, screen} from '@testing-library/svelte' +import {render, screen} from '@testing-library/svelte' +import userEvent from '@testing-library/user-event' -import Comp from '../Comp' +import Greeter from '../greeter.svelte' -test('shows proper heading when rendered', () => { - render(Comp, {name: 'World'}) - const heading = screen.getByText('Hello World!') - expect(heading).toBeInTheDocument() +test('no initial greeting', () => { + render(Greeter, {name: 'World'}) + + const button = screen.getByRole('button', {name: 'Greet'}) + const greeting = screen.queryByText(/hello/iu) + + 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() +}) +``` + +## Events + +Events can be tested using spy functions. Function props are more +straightforward to use and test than events, so consider using them if they make +sense for your project. + +### Component + +```html filename="src/button-with-event.svelte" + +``` + +```html filename="src/button-with-prop.svelte" + + + +``` + +### Tests + +```js filename="src/__tests__/button.test.ts" +import {test, vi} from 'vitest' + +import {render, screen} from '@testing-library/svelte' +import userEvent from '@testing-library/user-event' + +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 userEvent.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 userEvent.click(button) + + expect(onClick).toHaveBeenCalledOnce() +}) +``` + +## Slots + +To test slots, create a wrapper component for your test. Since slots are a +developer-facing API, test IDs can be helpful. + +### Component + +```html filename="src/heading.svelte" +

+ +

+``` + +### Tests + +```html filename="src/__tests__/heading.test.svelte" + + + + + +``` + +```js filename="src/__tests__/heading.test.js" +import {test} from 'vitest' +import {render, screen, within} from '@testing-library/svelte' + +import HeadingTest from './heading.test.svelte' + +test('heading with slot', () => { + render(HeadingTest) - // 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) + const heading = screen.getByRole('heading') + const child = within(heading).getByTestId('child') - expect(button).toHaveTextContent('Button Clicked') + 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 can be difficult to test directly. It's usually best 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 stores to test the +binding itself. + +### Component + +```html filename="src/text-input.svelte" + + + +``` + +### Tests + +```html filename="src/__tests__/text-input.test.svelte" + + + +``` + +```js filename="src/__tests__/text-input.test.js" +import {test} from 'vitest' + +import {render, screen} from '@testing-library/svelte' +import userEvent from '@testing-library/user-event' +import {get, writable} from 'svelte/store' + +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 any props in the `props` key. + +[component-options]: ./api.mdx#component-options + +### Components + +```html filename="src/messages-provider.svelte" + +``` + +```html filename="src/notifications.svelte" + + +
+ {#each $messages as message (message.id)} +

{message.text}

+ {/each} +
+``` + +### Tests + +```js filename="src/__tests__/notifications.test.js" +import {test} from 'vitest' + +import {render, screen} from '@testing-library/svelte' +import userEvent from '@testing-library/user-event' +import {readable} from 'svelte/store' + +import Notifications from '../notifications.svelte' + +test('text input with value binding', async () => { + const messages = readable(['hello', 'world']) + + render(TextInputTest, { + context: new Map([['messages', messages]]), + props: {label: 'Notifications'}, + }) + + const status = screen.getByRole('status', {name: 'Notifications'}) + + expect(status).toHaveTextContent('hello world') +}) +``` From d4e4a6405d5c674411e23d38145ebbe0335112ae Mon Sep 17 00:00:00 2001 From: Mike Cousins Date: Sun, 11 Feb 2024 01:06:33 -0500 Subject: [PATCH 2/5] fixup: fix code block title, remove redundant ToC --- docs/svelte-testing-library/example.mdx | 56 +++++++------------------ 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/docs/svelte-testing-library/example.mdx b/docs/svelte-testing-library/example.mdx index 98870f87b..8ea3f494e 100644 --- a/docs/svelte-testing-library/example.mdx +++ b/docs/svelte-testing-library/example.mdx @@ -4,12 +4,6 @@ title: Example sidebar_label: Example --- -- [Basic](#basic) -- [Events](#events) -- [Slots](#slots) -- [Two-way data binding](#two-way-data-binding) -- [Contexts](#contexts) - 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]. @@ -19,9 +13,7 @@ recipes][testing-recipes]. ## Basic -### Component - -```html filename="src/greeter.svelte" +```html title="src/greeter.svelte" @@ -89,9 +77,7 @@ sense for your project. ``` -### Tests - -```js filename="src/__tests__/button.test.ts" +```js title="src/__tests__/button.test.ts" import {test, vi} from 'vitest' import {render, screen} from '@testing-library/svelte' @@ -131,17 +117,13 @@ test('button with function prop', async () => { To test slots, create a wrapper component for your test. Since slots are a developer-facing API, test IDs can be helpful. -### Component - -```html filename="src/heading.svelte" +```html title="src/heading.svelte"

``` -### Tests - -```html filename="src/__tests__/heading.test.svelte" +```html title="src/__tests__/heading.test.svelte" @@ -151,7 +133,7 @@ developer-facing API, test IDs can be helpful. ``` -```js filename="src/__tests__/heading.test.js" +```js title="src/__tests__/heading.test.js" import {test} from 'vitest' import {render, screen, within} from '@testing-library/svelte' @@ -174,12 +156,10 @@ 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 stores to test the +component, you can use a wrapper component and writable store to test the binding itself. -### Component - -```html filename="src/text-input.svelte" +```html title="src/text-input.svelte" @@ -187,9 +167,7 @@ binding itself. ``` -### Tests - -```html filename="src/__tests__/text-input.test.svelte" +```html title="src/__tests__/text-input.test.svelte" ``` -```html filename="src/notifications.svelte" +```html title="src/notifications.svelte" @@ -77,14 +87,13 @@ sense for your project. ``` -```js title="src/__tests__/button.test.ts" -import {test, vi} from 'vitest' - +```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' +import ButtonWithEvent from './button-with-event.svelte' +import ButtonWithProp from './button-with-prop.svelte' test('button with event', async () => { const user = userEvent.setup() @@ -94,7 +103,7 @@ test('button with event', async () => { component.$on('click', onClick) const button = screen.getByRole('button') - await userEvent.click(button) + await user.click(button) expect(onClick).toHaveBeenCalledOnce() }) @@ -106,26 +115,33 @@ test('button with function prop', async () => { render(ButtonWithProp, {onClick}) const button = screen.getByRole('button') - await userEvent.click(button) + await user.click(button) expect(onClick).toHaveBeenCalledOnce() }) ``` +[vi-fn]: https://vitest.dev/api/vi.html#vi-fn + ## Slots -To test slots, create a wrapper component for your test. Since slots are a -developer-facing API, test IDs can be helpful. +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="src/heading.svelte" +```html title="heading.svelte"

``` -```html title="src/__tests__/heading.test.svelte" +```html title="heading.test.svelte" @@ -133,9 +149,9 @@ developer-facing API, test IDs can be helpful. ``` -```js title="src/__tests__/heading.test.js" -import {test} from 'vitest' +```js title="heading.test.js" import {render, screen, within} from '@testing-library/svelte' +import {expect, test} from 'vitest' import HeadingTest from './heading.test.svelte' @@ -151,15 +167,15 @@ test('heading with slot', () => { ## Two-way data binding -Two-way data binding can be difficult to test directly. It's usually best to -structure your code so that you can test the user-facing results, leaving the -binding as an implementation detail. +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="src/text-input.svelte" +```html title="text-input.svelte" @@ -167,9 +183,9 @@ binding itself. ``` -```html title="src/__tests__/text-input.test.svelte" +```html title="text-input.test.svelte" @@ -177,12 +193,11 @@ binding itself. ``` -```js title="src/__tests__/text-input.test.js" -import {test} from 'vitest' - +```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' @@ -203,11 +218,11 @@ test('text input with value binding', async () => { 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 any props in the `props` key. +`context`, be sure to place props under the `props` key. [component-options]: ./api.mdx#component-options -```html title="src/messages-provider.svelte" +```html title="notifications-provider.svelte" ``` -```html title="src/notifications.svelte" +```html title="notifications.svelte"