diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md
index 64bf49d77a27..6460528ab021 100644
--- a/documentation/docs/07-misc/02-testing.md
+++ b/documentation/docs/07-misc/02-testing.md
@@ -8,59 +8,37 @@ Testing helps you write and maintain your code and guard against regressions. Te
Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). You can use the Svelte CLI to [setup Vitest](/docs/cli/vitest) either during project creation or later on.
-To setup Vitest manually, first install it:
+The CLI will create two configurations with Vitest: one for client code (e.g. code inside components, the `.svelte` files, as well as code with runes, the [`.svelte.(js|ts)` files](/docs/svelte/svelte-js-files)), and another for server code (API logic, pure functions, server-side utilities). This configuration is in the `vite.config.(js|ts)` file.
-```bash
-npm install -D vitest
-```
-
-Then adjust your `vite.config.js`:
-
-
-```js
-/// file: vite.config.js
-import { defineConfig } from +++'vitest/config'+++;
-
-export default defineConfig({
- // ...
- // Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
- resolve: process.env.VITEST
- ? {
- conditions: ['browser']
- }
- : undefined
-});
-```
+## Testing server code
-> [!NOTE] If loading the browser version of all your packages is undesirable, because (for example) you also test backend libraries, [you may need to resort to an alias configuration](https://github.com/testing-library/svelte-testing-library/issues/222#issuecomment-1909993331)
+The default configuration will only pick up tests that are in files ending in `.(test|spec).(js|ts)`. These tests will run inside a Node.js environment, so you won't have access to any DOM APIs.
-You can now write unit tests for code inside your `.js/.ts` files:
+Note that server tests won't work properly with runes. See the section on client code testing on how to test code that uses runes (and even use runes inside your test code).
```js
-/// file: multiplier.svelte.test.js
-import { flushSync } from 'svelte';
+/// file: multiplier.test.js
+// @errors: 2307
import { expect, test } from 'vitest';
-import { multiplier } from './multiplier.svelte.js';
+import { multiplier } from './multiplier.js';
test('Multiplier', () => {
let double = multiplier(0, 2);
-
expect(double.value).toEqual(0);
double.set(5);
-
expect(double.value).toEqual(10);
});
```
```js
-/// file: multiplier.svelte.js
+/// file: multiplier.js
/**
* @param {number} initial
* @param {number} k
*/
export function multiplier(initial, k) {
- let count = $state(initial);
+ let count = initial;
return {
get value() {
@@ -74,44 +52,144 @@ export function multiplier(initial, k) {
}
```
-### Using runes inside your test files
+### Testing SSR (server-side rendering)
+
+You can ensure components rendered on the server contain certain content by directly using Svelte's [`render`](https://svelte.dev/docs/svelte/svelte-server#render) function on server code:
+
+```js
+/// file: Greeter.ssr.test.js
+import { describe, it, expect } from 'vitest';
+import { render } from 'svelte/server';
+import Greeter from './Greeter.svelte';
+
+describe('Greeter.svelte SSR', () => {
+ it('renders name', () => {
+ const { body } = render(Greeter, { props: { name: 'foo' } });
+ expect(body).toContain('foo');
+ });
+});
+```
+
+```svelte
+
+
+
+Hello, {name}!
+```
+
+## Testing client code
-Since Vitest processes your test files the same way as your source files, you can use runes inside your tests as long as the filename includes `.svelte`:
+It is possible to test your components in isolation using Vitest. The default configuration will only pick up tests that are in files ending in `.svelte.(test|spec).(js|ts)`. These tests will run inside a [jsdom](https://github.com/jsdom/jsdom) environment, which means it only emulates a web browser. You may need to define specific mocks if the browser APIs you're using don't exist in jsdom.
+
+> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component.
+
+In your test code, you'll need to import at least the `render` function from `@testing-library/svelte`:
```js
-/// file: multiplier.svelte.test.js
+/// file: Greeter.svelte.test.js
+import { describe, it, expect } from 'vitest';
+import { render } from '@testing-library/svelte';
import { flushSync } from 'svelte';
-import { expect, test } from 'vitest';
-import { multiplier } from './multiplier.svelte.js';
+import Greeter from './Greeter.svelte';
-test('Multiplier', () => {
- let count = $state(0);
- let double = multiplier(() => count, 2);
+describe('Greeter.svelte', () => {
+ it('shows name', () => {
+ const { container } = render(Greeter, { props: { name: 'Svelte Summit' } });
+ expect(container).toHaveTextContent('Svelte Summit');
+ });
+ it('can be tested with reactive state', () => {
+ const props = $state({ name: 'Svelte Summit' });
+ const { container } = render(Greeter, { props });
+ expect(container).toHaveTextContent('Svelte Summit');
+ props.name = 'Barcelona';
+ flushSync();
+ expect(container).not.toHaveTextContent('Svelte Summit');
+ expect(container).toHaveTextContent('Barcelona');
+ });
+});
+```
- expect(double.value).toEqual(0);
+```svelte
+
+
- count = 5;
+Hello, {name}!
+```
- expect(double.value).toEqual(10);
+In some cases, you might also find it useful to import `@testing-library/jest-dom/vitest` for its custom matchers (in the example below, we use `toBeInTheDocument()`):
+
+```js
+/// file: App.svelte.test.js
+import { describe, test, expect } from 'vitest';
+import '@testing-library/jest-dom/vitest';
+import { render, screen } from '@testing-library/svelte';
+import Page from './App.svelte';
+
+describe('/App.svelte', () => {
+ test('should render h1', () => {
+ render(Page);
+ expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
+ });
});
```
+```svelte
+
+
+
+ Hello world!
+
+
+```
+
+When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
+
+### Testing code with runes and using runes inside your test files
+
+You can test all code with runes in client tests. Since Vitest processes your test files the same way as your source files, you can also use runes inside your tests, as long as the test filename includes `.svelte`:
+
```js
-/// file: multiplier.svelte.js
-/**
- * @param {() => number} getCount
- * @param {number} k
- */
-export function multiplier(getCount, k) {
- return {
- get value() {
- return getCount() * k;
- }
- };
+/// file: doubler.svelte.test.js
+import { describe, expect, it } from 'vitest';
+import { Doubler } from './doubler.svelte.js';
+
+describe('doubler.svelte.js', () => {
+ it('should double initial value', () => {
+ let value = $state(1);
+ let doubler = new Doubler(() => value);
+ expect(doubler.value).toBe(2);
+ });
+ it('should be reactive', () => {
+ let value = $state(0);
+ let doubler = new Doubler(() => value);
+ expect(doubler.value).toBe(0);
+ value = 2;
+ expect(doubler.value).toBe(4);
+ });
+});
+
+```
+
+```js
+/// file: doubler.svelte.js
+// @errors: 2729 7006
+export class Doubler {
+ #getNumber;
+ #double = $derived(this.#getNumber() * 2);
+ constructor(getNumber) {
+ this.#getNumber = getNumber;
+ }
+ get value() {
+ return this.#double;
+ }
}
```
-If the code being tested uses effects, you need to wrap the test inside `$effect.root`:
+If the code being tested uses [effects](/docs/svelte/$effect), you need to wrap the test inside [`$effect.root`](/docs/svelte/$effect#$effect.root) and call [`flushSync`](/docs/svelte/svelte#flushSync) to force effects to run in the middle of test code.
```js
/// file: logger.svelte.test.js
@@ -133,7 +211,6 @@ test('Effect', () => {
count = 1;
flushSync();
-
expect(log.value).toEqual([0, 1]);
});
@@ -162,93 +239,176 @@ export function logger(getValue) {
}
```
-### Component testing
+### Adding additional jsdom mocks for client code
-It is possible to test your components in isolation using Vitest.
+The default configuration creates a `vitest-setup-client.(js|ts)` file where you can define mocks to "extend" the jsdom environment with additional browser APIs.
-> [!NOTE] Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component
+For example, if your code uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), you can mock it with code similar to the following:
-To get started, install jsdom (a library that shims DOM APIs):
+```js
+import { vi } from 'vitest';
+
+const localStorageMock = {
+ clear: vi.fn(),
+ getItem: vi.fn(),
+ key: vi.fn(),
+ removeItem: vi.fn(),
+ setItem: vi.fn(),
+}
+Object.defineProperty(window, 'localStorage', {
+ value: localStorageMock,
+})
+```
+
+### Testing client code with a real browser
+
+In more complex cases, it might be undesirable to test client code with jsdom because of jsdom's missing browser APIs, subtle behavior differences compared to real browsers, or because of no actual visual feedback. It's possible to configure Vitest to use a real browser environment, driven by [Playwright](https://playwright.dev/).
+
+To do so, start by adding the required packages:
```bash
-npm install -D jsdom
+npm install -D @vitest/browser vitest-browser-svelte playwright
```
-Then adjust your `vite.config.js`:
+And adjust the client testing configuration in `vite.config.js`:
```js
/// file: vite.config.js
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
- plugins: [
- /* ... */
- ],
+// ...
+{
+ extends: './vite.config.js',
+ plugins: [svelteTesting()],
test: {
- // If you are testing components client-side, you need to setup a DOM environment.
- // If not all your files should have this environment, you can use a
- // `// @vitest-environment jsdom` comment at the top of the test files instead.
- environment: 'jsdom'
+ name: 'client',
+ environment: +++'browser'+++,
++++ browser: {+++
++++ enabled: true,+++
++++ provider: 'playwright',+++
++++ instances: [{+++
++++ browser: 'chromium'+++
++++ }]+++
++++ },+++
+ // ...
},
- // Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
- resolve: process.env.VITEST
- ? {
- conditions: ['browser']
- }
- : undefined
-});
+},
+// ...
```
-After that, you can create a test file in which you import the component to test, interact with it programmatically and write expectations about the results:
+Since this is a real browser environment, the mocks in `vitest-setup-client.js` can be removed, and replaced with only the following code for additional matchers in test code:
```js
-/// file: component.test.js
-import { flushSync, mount, unmount } from 'svelte';
-import { expect, test } from 'vitest';
-import Component from './Component.svelte';
+// @errors: 2688
+///
+///
+```
+
+If you already had test code using the jsdom environment, you'll need to replace imports from the `@testing-library/svelte` library with imports from the `vitest-browser-svelte` library:
+
+```js
+/// file: Greeter.svelte.test.js
+import { describe, it, expect } from 'vitest';
+---import { render } from '@testing-library/svelte';---
++++import { render } from 'vitest-browser-svelte';+++
+import { flushSync } from 'svelte';
+import Greeter from './Greeter.svelte';
-test('Component', () => {
- // Instantiate the component using Svelte's `mount` API
- const component = mount(Component, {
- target: document.body, // `document` exists because of jsdom
- props: { initial: 0 }
+describe('Greeter.svelte', () => {
+ it('shows name', () => {
+ const { container } = render(Greeter, { props: { name: 'Svelte Summit' } });
+ expect(container).toHaveTextContent('Svelte Summit');
});
+ it('can be tested with reactive state', () => {
+ const props = $state({ name: 'Svelte Summit' });
+ const { container } = render(Greeter, { props });
+ expect(container).toHaveTextContent('Svelte Summit');
+ props.name = 'Barcelona';
+ flushSync();
+ expect(container).not.toHaveTextContent('Svelte Summit');
+ expect(container).toHaveTextContent('Barcelona');
+ });
+});
+```
- expect(document.body.innerHTML).toBe('');
+```svelte
+/// file: Greeter.svelte
+
- // Click the button, then flush the changes so you can synchronously write expectations
- document.body.querySelector('button').click();
- flushSync();
+Hello, {name}!
+```
- expect(document.body.innerHTML).toBe('');
+For more context and examples on testing Svelte components with real browsers, Scott Spence wrote [a blog post](https://scottspence.com/posts/migrating-from-testing-library-svelte-to-vitest-browser-svelte) expanding on a talk given at Svelte Summit 2025.
- // Remove the component from the DOM
- unmount(component);
-});
+## Manual unit and integration testing setup
+
+To setup Vitest manually, first install it and the additional libraries we need for both server-side and client-side tests (using jsdom):
+
+```bash
+npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
```
-While the process is very straightforward, it is also low level and somewhat brittle, as the precise structure of your component may change frequently. Tools like [@testing-library/svelte](https://testing-library.com/docs/svelte-testing-library/intro/) can help streamline your tests. The above test could be rewritten like this:
+Then adjust your `vite.config.js`:
+
```js
-/// file: component.test.js
-import { render, screen } from '@testing-library/svelte';
-import userEvent from '@testing-library/user-event';
-import { expect, test } from 'vitest';
-import Component from './Component.svelte';
+/// file: vite.config.js
+import { defineConfig } from +++'vitest/config'+++;
++++import { svelteTesting } from '@testing-library/svelte/vite'+++;
-test('Component', async () => {
- const user = userEvent.setup();
- render(Component);
+export default defineConfig({
+ // ...
+ test: {
+ workspace: [
+ {
+ extends: './vite.config.js',
+ test: {
+ name: 'server',
+ environment: 'node',
+ include: ['src/**/*.{test,spec}.{js,ts}'],
+ exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
+ },
+ },
+ {
+ extends: './vite.config.js',
+ plugins: [svelteTesting()],
+ test: {
+ name: 'client',
+ environment: 'jsdom',
+ include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
+ exclude: ['src/lib/server/**'],
+ setupFiles: ['./vitest-setup-client.js'],
+ },
+ },
+ ],
+ },
+});
+```
- const button = screen.getByRole('button');
- expect(button).toHaveTextContent(0);
+And create the file `vitest-setup-client.js` with a mock required by Svelte:
- await user.click(button);
- expect(button).toHaveTextContent(1);
+
+```js
+/// file: vitest-setup-client.js
+import '@testing-library/jest-dom/vitest';
+import { vi } from 'vitest';
+
+// required for svelte5 + jsdom as jsdom does not support matchMedia
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ enumerable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
});
-```
-When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
+// add more mocks here if you need them
+```
## E2E tests using Playwright