From 2d425a48e63a114be77651ed63720ae4bf3b01e1 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Sun, 25 May 2025 15:00:42 -0400 Subject: [PATCH] docs(examples): add examples to repository --- .all-contributorsrc | 2 +- .github/workflows/release.yml | 3 +- CONTRIBUTING.md | 4 +- README.md | 16 +-- eslint.config.js | 5 +- examples/basic/basic.svelte | 13 +++ examples/basic/basic.test.js | 26 +++++ examples/basic/readme.md | 68 +++++++++++ examples/binds/bind.svelte | 5 + examples/binds/bind.test.js | 24 ++++ examples/binds/no-bind.svelte | 9 ++ examples/binds/readme.md | 82 +++++++++++++ examples/contexts/context.svelte | 14 +++ examples/contexts/context.test.js | 24 ++++ examples/contexts/readme.md | 61 ++++++++++ examples/deprecated/deprecated-event.svelte | 1 + examples/deprecated/deprecated-event.test.js | 17 +++ examples/deprecated/deprecated-slot.svelte | 3 + examples/deprecated/deprecated-slot.test.js | 13 +++ .../deprecated/deprecated-slot.test.svelte | 7 ++ examples/deprecated/readme.md | 105 +++++++++++++++++ examples/events/event.svelte | 5 + examples/events/event.test.js | 17 +++ examples/events/readme.md | 43 +++++++ examples/readme.md | 8 ++ examples/snippets/basic-snippet.svelte | 7 ++ examples/snippets/basic-snippet.test.js | 13 +++ examples/snippets/basic-snippet.test.svelte | 7 ++ examples/snippets/complex-snippet.svelte | 9 ++ examples/snippets/complex-snippet.test.js | 18 +++ examples/snippets/readme.md | 108 ++++++++++++++++++ package.json | 14 ++- prettier.config.js | 7 ++ 33 files changed, 741 insertions(+), 17 deletions(-) create mode 100644 examples/basic/basic.svelte create mode 100644 examples/basic/basic.test.js create mode 100644 examples/basic/readme.md create mode 100644 examples/binds/bind.svelte create mode 100644 examples/binds/bind.test.js create mode 100644 examples/binds/no-bind.svelte create mode 100644 examples/binds/readme.md create mode 100644 examples/contexts/context.svelte create mode 100644 examples/contexts/context.test.js create mode 100644 examples/contexts/readme.md create mode 100644 examples/deprecated/deprecated-event.svelte create mode 100644 examples/deprecated/deprecated-event.test.js create mode 100644 examples/deprecated/deprecated-slot.svelte create mode 100644 examples/deprecated/deprecated-slot.test.js create mode 100644 examples/deprecated/deprecated-slot.test.svelte create mode 100644 examples/deprecated/readme.md create mode 100644 examples/events/event.svelte create mode 100644 examples/events/event.test.js create mode 100644 examples/events/readme.md create mode 100644 examples/readme.md create mode 100644 examples/snippets/basic-snippet.svelte create mode 100644 examples/snippets/basic-snippet.test.js create mode 100644 examples/snippets/basic-snippet.test.svelte create mode 100644 examples/snippets/complex-snippet.svelte create mode 100644 examples/snippets/complex-snippet.test.js create mode 100644 examples/snippets/readme.md diff --git a/.all-contributorsrc b/.all-contributorsrc index fab0bfd..d887ad9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -109,7 +109,7 @@ "name": "Michael Cousins", "avatar_url": "https://avatars.githubusercontent.com/u/2963448?v=4", "profile": "https://michael.cousins.io/", - "contributions": ["code"] + "contributions": ["code", "doc", "ideas", "maintenance", "test"] } ], "contributorsPerLine": 7, diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be02b70..136111f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,8 +37,9 @@ jobs: - { svelte: '5', node: '16' } - { svelte: '5', node: '18' } include: - # We only need to lint once, so do it on latest Node and Svelte + # Only lint and test examples on latest Node and Svelte - { svelte: '5', node: '22', check: 'lint' } + - { svelte: '5', node: '22', check: 'test:examples' } # Run type checks in latest applicable Node - { svelte: '3', node: '20', check: 'types:legacy' } - { svelte: '4', node: '22', check: 'types:legacy' } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41193a3..328bcda 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,10 +77,10 @@ npm run all:legacy ### Docs -Use the `toc` script to ensure the README's table of contents is up to date: +Use the `docs` script to ensure the README's table of contents is up to date: ```shell -npm run toc +npm run docs ``` Use `contributors:add` to add a contributor to the README: diff --git a/README.md b/README.md index 29429d4..f94a64e 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@

Simple and complete Svelte testing utilities that encourage good testing practices.

-[**Read The Docs**][stl-docs] | [Edit the docs][stl-docs-repo] +[**Read The Docs**][stl-docs] | [Edit the docs][stl-docs-repo] | [Examples](./examples) + [![Build Status][build-badge]][build] [![Code Coverage][coverage-badge]][coverage] [![version][version-badge]][package] @@ -29,7 +30,9 @@ [![Watch on GitHub][github-watch-badge]][github-watch] [![Star on GitHub][github-star-badge]][github-star] [![Tweet][twitter-badge]][twitter] + +
@@ -63,9 +66,6 @@ ## Table of Contents - - - - [The Problem](#the-problem) - [This Solution](#this-solution) - [Installation](#installation) @@ -78,8 +78,6 @@ - [❓ Questions](#-questions) - [Contributors](#contributors) - - ## The Problem You want to write maintainable tests for your [Svelte][svelte] components. @@ -217,8 +215,11 @@ instead of filing an issue on GitHub. Thanks goes to these people ([emoji key][emojis]): + + + @@ -240,12 +241,13 @@ Thanks goes to these people ([emoji key][emojis]): - +
Yanick Champoux
Yanick Champoux

💻
Michael Cousins
Michael Cousins

💻
Michael Cousins
Michael Cousins

💻 📖 🤔 🚧 ⚠️
+ diff --git a/eslint.config.js b/eslint.config.js index c03de8c..092d967 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -75,7 +75,10 @@ export default tseslint.config( files: ['**/*.svelte'], rules: { 'svelte/no-unused-svelte-ignore': 'off', - 'unicorn/filename-case': ['error', { case: 'pascalCase' }], + 'unicorn/filename-case': [ + 'error', + { cases: { kebabCase: true, pascalCase: true } }, + ], 'unicorn/no-useless-undefined': 'off', }, }, diff --git a/examples/basic/basic.svelte b/examples/basic/basic.svelte new file mode 100644 index 0000000..11ca22c --- /dev/null +++ b/examples/basic/basic.svelte @@ -0,0 +1,13 @@ + + + + +{#if showGreeting} +

Hello {name}

+{/if} diff --git a/examples/basic/basic.test.js b/examples/basic/basic.test.js new file mode 100644 index 0000000..6ea6099 --- /dev/null +++ b/examples/basic/basic.test.js @@ -0,0 +1,26 @@ +import { render, screen } from '@testing-library/svelte' +import { userEvent } from '@testing-library/user-event' +import { expect, test } from 'vitest' + +import Subject from './basic.svelte' + +test('no initial greeting', () => { + render(Subject, { name: 'World' }) + + const button = screen.getByRole('button', { name: 'Greet' }) + const greeting = screen.queryByText(/hello/iu) + + expect(button).toBeInTheDocument() + expect(greeting).not.toBeInTheDocument() +}) + +test('greeting appears on click', async () => { + const user = userEvent.setup() + render(Subject, { name: 'World' }) + + const button = screen.getByRole('button') + await user.click(button) + const greeting = screen.getByText(/hello world/iu) + + expect(greeting).toBeInTheDocument() +}) diff --git a/examples/basic/readme.md b/examples/basic/readme.md new file mode 100644 index 0000000..6d98485 --- /dev/null +++ b/examples/basic/readme.md @@ -0,0 +1,68 @@ +# 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][] +- Make assertions using expect, using matchers from + [@testing-library/jest-dom][] + +[query]: https://testing-library.com/docs/queries/about +[render()]: https://testing-library.com/docs/svelte-testing-library/api#render +[@testing-library/user-event]: https://testing-library.com/docs/user-event/intro +[@testing-library/jest-dom]: https://github.com/testing-library/jest-dom + +## Table of contents + +- [`basic.svelte`](#basicsvelte) +- [`basic.test.js`](#basictestjs) + +## `basic.svelte` + +```svelte file=./basic.svelte + + + + +{#if showGreeting} +

Hello {name}

+{/if} +``` + +## `basic.test.js` + +```js file=./basic.test.js +import { render, screen } from '@testing-library/svelte' +import { userEvent } from '@testing-library/user-event' +import { expect, test } from 'vitest' + +import Subject from './basic.svelte' + +test('no initial greeting', () => { + render(Subject, { name: 'World' }) + + const button = screen.getByRole('button', { name: 'Greet' }) + const greeting = screen.queryByText(/hello/iu) + + expect(button).toBeInTheDocument() + expect(greeting).not.toBeInTheDocument() +}) + +test('greeting appears on click', async () => { + const user = userEvent.setup() + render(Subject, { name: 'World' }) + + const button = screen.getByRole('button') + await user.click(button) + const greeting = screen.getByText(/hello world/iu) + + expect(greeting).toBeInTheDocument() +}) +``` diff --git a/examples/binds/bind.svelte b/examples/binds/bind.svelte new file mode 100644 index 0000000..62fd858 --- /dev/null +++ b/examples/binds/bind.svelte @@ -0,0 +1,5 @@ + + + diff --git a/examples/binds/bind.test.js b/examples/binds/bind.test.js new file mode 100644 index 0000000..6d00665 --- /dev/null +++ b/examples/binds/bind.test.js @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/svelte' +import { userEvent } from '@testing-library/user-event' +import { expect, test } from 'vitest' + +import Subject from './bind.svelte' + +test('value binding', async () => { + const user = userEvent.setup() + let value = '' + + render(Subject, { + get value() { + return value + }, + set value(nextValue) { + value = nextValue + }, + }) + + const input = screen.getByRole('textbox') + await user.type(input, 'hello world') + + expect(value).toBe('hello world') +}) diff --git a/examples/binds/no-bind.svelte b/examples/binds/no-bind.svelte new file mode 100644 index 0000000..3e4079a --- /dev/null +++ b/examples/binds/no-bind.svelte @@ -0,0 +1,9 @@ + + + diff --git a/examples/binds/readme.md b/examples/binds/readme.md new file mode 100644 index 0000000..2506fa9 --- /dev/null +++ b/examples/binds/readme.md @@ -0,0 +1,82 @@ +# Binds + +Two-way data binding using [bindable() props][] is difficult to test directly. +It's usually easier to structure your code so that you can test 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 setters to test your binding. + +[bindable() props]: https://svelte.dev/docs/svelte/$bindable + +## Table of contents + +- [`bind.svelte`](#bindsvelte) +- [`bind.test.js`](#bindtestjs) +- [Consider avoiding binding](#consider-avoiding-binding) + +## `bind.svelte` + +```svelte file=./bind.svelte + + + +``` + +## `bind.test.js` + +```svelte file=./bind.test.js +import { render, screen } from '@testing-library/svelte' +import { userEvent } from '@testing-library/user-event' +import { expect, test } from 'vitest' + +import Subject from './bind.svelte' + +test('value binding', async () => { + const user = userEvent.setup() + let value = '' + + render(Subject, { + get value() { + return value + }, + set value(nextValue) { + value = nextValue + }, + }) + + const input = screen.getByRole('textbox') + await user.type(input, 'hello world') + + expect(value).toBe('hello world') +}) +``` + +## Consider avoiding binding + +Before embarking on writing tests for bindable props, consider avoiding +`bindable()` entirely. Two-way data binding can make your data flows and state +changes difficult to reason about and test effectively. Instead, you can use +value props to pass data down and callback props to pass changes back up to the +parent. + +> Well-written applications use bindings very sparingly — the vast majority of +> data flow should be top-down -- +> [Rich Harris](https://github.com/sveltejs/svelte/issues/10768#issue-2181814844) + +For example, rather than using a `bindable()` prop, use a value prop to pass the +value down and callback prop to send changes back up to the parent: + +```svelte file=./no-bind.svelte + + + +``` diff --git a/examples/contexts/context.svelte b/examples/contexts/context.svelte new file mode 100644 index 0000000..c0fbb19 --- /dev/null +++ b/examples/contexts/context.svelte @@ -0,0 +1,14 @@ + + +
+ {#each messages.current as message (message.id)} +

{message.text}

+
+ {/each} +
diff --git a/examples/contexts/context.test.js b/examples/contexts/context.test.js new file mode 100644 index 0000000..4a433d2 --- /dev/null +++ b/examples/contexts/context.test.js @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/svelte' +import { expect, test } from 'vitest' + +import Subject from './context.svelte' + +test('notifications with messages from context', async () => { + const messages = { + get current() { + return [ + { id: 'abc', text: 'hello' }, + { id: 'def', text: 'world' }, + ] + }, + } + + render(Subject, { + context: new Map([['messages', messages]]), + props: { label: 'Notifications' }, + }) + + const status = screen.getByRole('status', { name: 'Notifications' }) + + expect(status).toHaveTextContent('hello world') +}) diff --git a/examples/contexts/readme.md b/examples/contexts/readme.md new file mode 100644 index 0000000..15284a9 --- /dev/null +++ b/examples/contexts/readme.md @@ -0,0 +1,61 @@ +# Context + +If your component requires access to contexts, you can pass those contexts in +when you render the component. When using extra [component options][] like +`context`, be sure to place props under the `props` key. + +[component options]: + https://testing-library.com/docs/svelte-testing-library/api#component-options + +## Table of contents + +- [`context.svelte`](#contextsvelte) +- [`context.test.js`](#contexttestjs) + +## `context.svelte` + +```svelte file=./context.svelte + + +
+ {#each messages.current as message (message.id)} +

{message.text}

+
+ {/each} +
+``` + +## `context.test.js` + +```js file=./context.test.js +import { render, screen } from '@testing-library/svelte' +import { expect, test } from 'vitest' + +import Subject from './context.svelte' + +test('notifications with messages from context', async () => { + const messages = { + get current() { + return [ + { id: 'abc', text: 'hello' }, + { id: 'def', text: 'world' }, + ] + }, + } + + render(Subject, { + context: new Map([['messages', messages]]), + props: { label: 'Notifications' }, + }) + + const status = screen.getByRole('status', { name: 'Notifications' }) + + expect(status).toHaveTextContent('hello world') +}) +``` diff --git a/examples/deprecated/deprecated-event.svelte b/examples/deprecated/deprecated-event.svelte new file mode 100644 index 0000000..fe21a0e --- /dev/null +++ b/examples/deprecated/deprecated-event.svelte @@ -0,0 +1 @@ + diff --git a/examples/deprecated/deprecated-event.test.js b/examples/deprecated/deprecated-event.test.js new file mode 100644 index 0000000..973e897 --- /dev/null +++ b/examples/deprecated/deprecated-event.test.js @@ -0,0 +1,17 @@ +import { render, screen } from '@testing-library/svelte' +import userEvent from '@testing-library/user-event' +import { expect, test, vi } from 'vitest' + +import Subject from './deprecated-event.svelte' + +test('on:click event', async () => { + const user = userEvent.setup() + const onClick = vi.fn() + + render(Subject, { events: { click: onClick } }) + + const button = screen.getByRole('button') + await user.click(button) + + expect(onClick).toHaveBeenCalledOnce() +}) diff --git a/examples/deprecated/deprecated-slot.svelte b/examples/deprecated/deprecated-slot.svelte new file mode 100644 index 0000000..08d88e3 --- /dev/null +++ b/examples/deprecated/deprecated-slot.svelte @@ -0,0 +1,3 @@ +

+ +

diff --git a/examples/deprecated/deprecated-slot.test.js b/examples/deprecated/deprecated-slot.test.js new file mode 100644 index 0000000..dfe73b8 --- /dev/null +++ b/examples/deprecated/deprecated-slot.test.js @@ -0,0 +1,13 @@ +import { render, screen, within } from '@testing-library/svelte' +import { expect, test } from 'vitest' + +import SubjectTest from './deprecated-slot.test.svelte' + +test('heading with slot', () => { + render(SubjectTest) + + const heading = screen.getByRole('heading') + const child = within(heading).getByTestId('child') + + expect(child).toBeInTheDocument() +}) diff --git a/examples/deprecated/deprecated-slot.test.svelte b/examples/deprecated/deprecated-slot.test.svelte new file mode 100644 index 0000000..92b5f6b --- /dev/null +++ b/examples/deprecated/deprecated-slot.test.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/deprecated/readme.md b/examples/deprecated/readme.md new file mode 100644 index 0000000..ca1a8d1 --- /dev/null +++ b/examples/deprecated/readme.md @@ -0,0 +1,105 @@ +# Deprecated Svelte 3/4 features + +Several features from Svelte 3 and 4 have been deprecated in Svelte 5, but while +you still have components using the old syntax, or if you haven't yet updated to +Svelte 5, you can continue to use `@testing-library/svelte` to test your +components. + +## Table of contents + +- [Events](#events) + - [`deprecated-event.svelte`](#deprecated-eventsvelte) + - [`deprecated-event.test.js`](#deprecated-eventtestjs) +- [Slots](#slots) + - [`deprecated-slot.svelte`](#deprecated-slotsvelte) + - [`deprecated-slot.test.svelte`](#deprecated-slottestsvelte) + - [`deprecated-slot.test.js`](#deprecated-slottestjs) + +## Events + +The `on:event` syntax was deprecated in favor of callback props. However, if you +have updated your Svelte runtime to version 5, you can use the `events` +component option to continue to test events in older components. + +### `deprecated-event.svelte` + +```svelte file=./deprecated-event.svelte + +``` + +### `deprecated-event.test.js` + +> \[!WARNING] +> +> If you are still using Svelte version 3 or 4, `render` will **not** have an +> `events` option. Instead, use `component.$on` to attach an event listener. +> +> ```js +> const onClick = vi.fn() +> +> const { component } = render(Subject) +> component.$on('click', onClick) +> ``` + +```js file=./deprecated-event.test.js +import { render, screen } from '@testing-library/svelte' +import userEvent from '@testing-library/user-event' +import { expect, test, vi } from 'vitest' + +import Subject from './deprecated-event.svelte' + +test('on:click event', async () => { + const user = userEvent.setup() + const onClick = vi.fn() + + render(Subject, { events: { click: onClick } }) + + const button = screen.getByRole('button') + await user.click(button) + + expect(onClick).toHaveBeenCalledOnce() +}) +``` + +## Slots + +The slots feature was deprecated in favor of snippets. If you have components +that still use slots, you can create a wrapper component to test them. + +### `deprecated-slot.svelte` + +```svelte file=./deprecated-slot.svelte +

+ +

+``` + +### `deprecated-slot.test.svelte` + +```svelte file=./deprecated-slot.test.svelte + + + + + +``` + +### `deprecated-slot.test.js` + +```js file=deprecated-slot.test.js +import { render, screen, within } from '@testing-library/svelte' +import { expect, test } from 'vitest' + +import SubjectTest from './deprecated-slot.test.svelte' + +test('heading with slot', () => { + render(SubjectTest) + + const heading = screen.getByRole('heading') + const child = within(heading).getByTestId('child') + + expect(child).toBeInTheDocument() +}) +``` diff --git a/examples/events/event.svelte b/examples/events/event.svelte new file mode 100644 index 0000000..3562da1 --- /dev/null +++ b/examples/events/event.svelte @@ -0,0 +1,5 @@ + + + diff --git a/examples/events/event.test.js b/examples/events/event.test.js new file mode 100644 index 0000000..c37902b --- /dev/null +++ b/examples/events/event.test.js @@ -0,0 +1,17 @@ +import { render, screen } from '@testing-library/svelte' +import { userEvent } from '@testing-library/user-event' +import { expect, test, vi } from 'vitest' + +import Subject from './event.svelte' + +test('onclick event', async () => { + const user = userEvent.setup() + const onclick = vi.fn() + + render(Subject, { onclick }) + + const button = screen.getByRole('button') + await user.click(button) + + expect(onclick).toHaveBeenCalledOnce() +}) diff --git a/examples/events/readme.md b/examples/events/readme.md new file mode 100644 index 0000000..7af235a --- /dev/null +++ b/examples/events/readme.md @@ -0,0 +1,43 @@ +# Events + +Events can be tested using spy functions. If you're using Vitest you can use +[vi.fn()][] to create a spy. + +[vi.fn()]: https://vitest.dev/api/vi.html#vi-fn + +## Table of contents + +- [`event.svelte`](#eventsvelte) +- [`event.test.js`](#eventtestjs) + +## `event.svelte` + +```svelte file=./event.svelte + + + +``` + +## `event.test.js` + +```js file=./event.test.js +import { render, screen } from '@testing-library/svelte' +import { userEvent } from '@testing-library/user-event' +import { expect, test, vi } from 'vitest' + +import Subject from './event.svelte' + +test('onclick event', async () => { + const user = userEvent.setup() + const onclick = vi.fn() + + render(Subject, { onclick }) + + const button = screen.getByRole('button') + await user.click(button) + + expect(onclick).toHaveBeenCalledOnce() +}) +``` diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 0000000..11d4c92 --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,8 @@ +# `@testing-library/svelte` examples + +- [Basic](./basic) +- [Events](./events) +- [Snippets](./snippets) +- [Contexts]('./contexts) +- [Binds](./binds) +- [Deprecated Svelte 3 and 4 features](./deprecated) diff --git a/examples/snippets/basic-snippet.svelte b/examples/snippets/basic-snippet.svelte new file mode 100644 index 0000000..55e1f71 --- /dev/null +++ b/examples/snippets/basic-snippet.svelte @@ -0,0 +1,7 @@ + + +

+ {@render children?.()} +

diff --git a/examples/snippets/basic-snippet.test.js b/examples/snippets/basic-snippet.test.js new file mode 100644 index 0000000..11cf95c --- /dev/null +++ b/examples/snippets/basic-snippet.test.js @@ -0,0 +1,13 @@ +import { render, screen, within } from '@testing-library/svelte' +import { expect, test } from 'vitest' + +import SubjectTest from './basic-snippet.test.svelte' + +test('basic snippet', () => { + render(SubjectTest) + + const heading = screen.getByRole('heading') + const child = within(heading).getByTestId('child') + + expect(child).toBeInTheDocument() +}) diff --git a/examples/snippets/basic-snippet.test.svelte b/examples/snippets/basic-snippet.test.svelte new file mode 100644 index 0000000..e96cac7 --- /dev/null +++ b/examples/snippets/basic-snippet.test.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/snippets/complex-snippet.svelte b/examples/snippets/complex-snippet.svelte new file mode 100644 index 0000000..81e8a70 --- /dev/null +++ b/examples/snippets/complex-snippet.svelte @@ -0,0 +1,9 @@ + + +

+ {@render message?.(greeting)} +

diff --git a/examples/snippets/complex-snippet.test.js b/examples/snippets/complex-snippet.test.js new file mode 100644 index 0000000..55885ef --- /dev/null +++ b/examples/snippets/complex-snippet.test.js @@ -0,0 +1,18 @@ +import { render, screen } from '@testing-library/svelte' +import { createRawSnippet } from 'svelte' +import { expect, test } from 'vitest' + +import Subject from './complex-snippet.svelte' + +test('renders greeting in message snippet', () => { + render(Subject, { + name: 'Alice', + message: createRawSnippet((greeting) => ({ + render: () => `${greeting()}`, + })), + }) + + const message = screen.getByTestId('message') + + expect(message).toHaveTextContent('Hello, Alice!') +}) diff --git a/examples/snippets/readme.md b/examples/snippets/readme.md new file mode 100644 index 0000000..30a70d6 --- /dev/null +++ b/examples/snippets/readme.md @@ -0,0 +1,108 @@ +# Snippets + +Snippets are difficult to test directly. It's usually easier to structure your +code so that you can test the user-facing results, leaving any snippets as an +implementation detail. However, if snippets are an important developer-facing +API of your component, there are several strategies you can use. + +## Table of contents + +- [Basic snippets example](#basic-snippets-example) + - [`basic-snippet.svelte`](#basic-snippetsvelte) + - [`basic-snippet.test.svelte`](#basic-snippettestsvelte) + - [`basic-snippet.test.js`](#basic-snippettestjs) +- [Using `createRawSnippet`](#using-createrawsnippet) + - [`complex-snippet.svelte`](#complex-snippetsvelte) + - [`complex-snippet.test.js`](#complex-snippettestjs) + +## Basic snippets example + +For simple snippets, you can use a wrapper component and "dummy" children to +test them. Setting `data-testid` attributes can be helpful when testing slots in +this manner. + +### `basic-snippet.svelte` + +```svelte file=./basic-snippet.svelte + + +

+ {@render children?.()} +

+``` + +### `basic-snippet.test.svelte` + +```svelte file=./basic-snippet.test.svelte + + + + + +``` + +### `basic-snippet.test.js` + +```js file=./basic-snippet.test.js +import { render, screen, within } from '@testing-library/svelte' +import { expect, test } from 'vitest' + +import SubjectTest from './basic-snippet.test.svelte' + +test('basic snippet', () => { + render(SubjectTest) + + const heading = screen.getByRole('heading') + const child = within(heading).getByTestId('child') + + expect(child).toBeInTheDocument() +}) +``` + +## Using `createRawSnippet` + +For more complex snippets, e.g. where you want to check arguments, you can use +Svelte's [createRawSnippet][] API. + +[createRawSnippet]: https://svelte.dev/docs/svelte/svelte#createRawSnippet + +### `complex-snippet.svelte` + +```svelte file=./complex-snippet.svelte + + +

+ {@render message?.(greeting)} +

+``` + +### `complex-snippet.test.js` + +```js file=./complex-snippet.test.js +import { render, screen } from '@testing-library/svelte' +import { createRawSnippet } from 'svelte' +import { expect, test } from 'vitest' + +import Subject from './complex-snippet.svelte' + +test('renders greeting in message snippet', () => { + render(Subject, { + name: 'Alice', + message: createRawSnippet((greeting) => ({ + render: () => `${greeting()}`, + })), + }) + + const message = screen.getByTestId('message') + + expect(message).toHaveTextContent('Hello, Alice!') +}) +``` diff --git a/package.json b/package.json index fea66a7..14e8cf9 100644 --- a/package.json +++ b/package.json @@ -52,17 +52,18 @@ "types" ], "scripts": { - "all": "npm-run-all contributors:generate toc format types build test:vitest:* test:jest", + "all": "npm-run-all contributors:generate docs format types build test:vitest:* test:jest test:examples", "all:legacy": "npm-run-all types:legacy test:vitest:* test:jest", - "toc": "doctoc README.md", + "docs": "remark --output --use remark-toc --use remark-code-import --use unified-prettier README.md examples", "lint": "prettier . --check && eslint .", "format": "prettier . --write && eslint . --fix", "setup": "npm run install:5 && npm run all", "test": "vitest run --coverage", "test:watch": "vitest", - "test:vitest:jsdom": "vitest run --coverage --environment jsdom", - "test:vitest:happy-dom": "vitest run --coverage --environment happy-dom", + "test:vitest:jsdom": "vitest run tests --coverage --environment jsdom", + "test:vitest:happy-dom": "vitest run tests --coverage --environment happy-dom", "test:jest": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --coverage", + "test:examples": "vitest run examples --coverage", "types": "svelte-check", "types:legacy": "svelte-check --tsconfig tsconfig.legacy.json", "build": "tsc -p tsconfig.build.json && cp src/component-types.d.ts types", @@ -98,7 +99,6 @@ "@vitest/coverage-v8": "^3.1.3", "@vitest/eslint-plugin": "^1.1.44", "all-contributors-cli": "^6.26.1", - "doctoc": "^2.2.1", "eslint": "^9.26.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-jest-dom": "^5.5.0", @@ -116,12 +116,16 @@ "npm-run-all": "^4.1.5", "prettier": "^3.5.3", "prettier-plugin-svelte": "^3.3.3", + "remark-cli": "^12.0.1", + "remark-code-import": "^1.2.0", + "remark-toc": "^9.0.0", "svelte": "^5.28.2", "svelte-check": "^4.1.7", "svelte-jester": "^5.0.0", "typescript": "^5.8.3", "typescript-eslint": "^8.32.0", "typescript-svelte-plugin": "^0.3.46", + "unified-prettier": "^2.0.1", "vite": "^6.3.5", "vitest": "^3.1.3" } diff --git a/prettier.config.js b/prettier.config.js index 8c5a52d..55343f2 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -10,5 +10,12 @@ export default { parser: 'svelte', }, }, + { + files: 'examples/**/*.md', + options: { + printWidth: 80, + proseWrap: 'always', + }, + }, ], }