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] + +Yanick Champoux 💻 |
|
Michael Cousins 💻 |
+ Michael Cousins 💻 📖 🤔 🚧 ⚠️ |
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 @@ + + +{message.text}
+{message.text}
++ {@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 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', + }, + }, ], }