Modern testing patterns for Svelte 5 using vitest-browser-svelte and Sveltest methodology. Use when users ask about testing Svelte components, unit tests, integration tests, vitest, vitest-browser-svelte, testing runes, testing $state/$derived/$effect, or setting up a test environment for Svelte 5.
Tests Svelte 5 components with vitest-browser-svelte for runes and DOM behavior.
npx claudepluginhub maxnoller/claude-code-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/Button.svelteexamples/button.svelte.test.tsexamples/counter.svelte.tsexamples/runes.svelte.test.tsModern Svelte 5 testing uses vitest-browser-svelte to run tests in real browsers instead of simulated environments like jsdom. This approach provides accurate testing of runes, reactivity, and DOM behavior.
pnpm install -D @vitest/browser-playwright vitest-browser-svelte playwright
# Remove old testing libraries if present
pnpm remove @testing-library/jest-dom @testing-library/svelte jsdom
// vite.config.ts
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
test: {
projects: [
{
name: 'client',
test: {
testTimeout: 2000,
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
},
},
{
name: 'server',
test: {
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
},
},
],
},
});
// src/vitest-setup-client.ts
/// <reference types="vitest/browser" />
/// <reference types="@vitest/browser-playwright" />
// src/lib/components/Button.svelte.test.ts
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import Button from './Button.svelte';
describe('Button', () => {
it('renders with text', async () => {
render(Button, { props: { label: 'Click me' } });
const button = page.getByRole('button', { name: 'Click me' });
await expect.element(button).toBeVisible();
});
it('handles click events', async () => {
let clicked = false;
render(Button, {
props: {
label: 'Click',
onclick: () => clicked = true
}
});
await page.getByRole('button').click();
expect(clicked).toBe(true);
});
});
Always prefer semantic queries in this order:
// 1. Roles (best - accessible)
page.getByRole('button', { name: 'Submit' });
page.getByRole('textbox', { name: 'Email' });
page.getByRole('link', { name: 'Home' });
// 2. Labels (forms)
page.getByLabel('Email address');
page.getByLabel('Password');
// 3. Text content
page.getByText('Welcome back');
page.getByText(/error/i);
// 4. Test IDs (last resort)
page.getByTestId('submit-button');
Vitest Browser Mode is strict—multiple matches throw errors:
// When multiple elements match, be specific
page.getByRole('listitem').first();
page.getByRole('listitem').nth(2);
page.getByRole('listitem').last();
// Or use more specific selectors
page.getByRole('listitem', { name: /important/i });
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import Counter from './Counter.svelte';
describe('Counter with $state', () => {
it('increments count', async () => {
render(Counter);
const button = page.getByRole('button', { name: 'Increment' });
const count = page.getByTestId('count');
await expect.element(count).toHaveTextContent('0');
await button.click();
await expect.element(count).toHaveTextContent('1');
await button.click();
await expect.element(count).toHaveTextContent('2');
});
});
// Component: DoubleCounter.svelte
// let count = $state(0);
// let doubled = $derived(count * 2);
describe('Derived values', () => {
it('updates derived when state changes', async () => {
render(DoubleCounter);
const count = page.getByTestId('count');
const doubled = page.getByTestId('doubled');
const button = page.getByRole('button', { name: '+' });
await expect.element(count).toHaveTextContent('0');
await expect.element(doubled).toHaveTextContent('0');
await button.click();
await expect.element(count).toHaveTextContent('1');
await expect.element(doubled).toHaveTextContent('2');
});
});
import { vi } from 'vitest';
describe('Effects', () => {
it('runs effect on state change', async () => {
const consoleSpy = vi.spyOn(console, 'log');
render(ComponentWithEffect);
await page.getByRole('button').click();
expect(consoleSpy).toHaveBeenCalledWith('Count changed:', 1);
consoleSpy.mockRestore();
});
});
// counter.svelte.ts
export function createCounter(initial = 0) {
let count = $state(initial);
return {
get count() { return count },
increment: () => count++
};
}
// counter.svelte.test.ts
import { describe, expect, it } from 'vitest';
import { flushSync, untrack } from 'svelte';
import { createCounter } from './counter.svelte';
describe('createCounter', () => {
it('increments count', () => {
const counter = createCounter(0);
expect(untrack(() => counter.count)).toBe(0);
counter.increment();
flushSync();
expect(untrack(() => counter.count)).toBe(1);
});
});
describe('LoginForm', () => {
it('submits with valid data', async () => {
const handleSubmit = vi.fn();
render(LoginForm, { props: { onsubmit: handleSubmit } });
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
it('shows validation errors', async () => {
render(LoginForm);
await page.getByRole('button', { name: 'Login' }).click();
await expect.element(page.getByText('Email is required')).toBeVisible();
await expect.element(page.getByText('Password is required')).toBeVisible();
});
});
describe('ContactForm', () => {
it('collects form data correctly', async () => {
let submittedData: FormData | null = null;
render(ContactForm, {
props: {
onsubmit: (data: FormData) => submittedData = data
}
});
await page.getByLabel('Name').fill('John Doe');
await page.getByLabel('Message').fill('Hello world');
await page.getByRole('button', { name: 'Send' }).click();
expect(submittedData?.get('name')).toBe('John Doe');
expect(submittedData?.get('message')).toBe('Hello world');
});
});
describe('AsyncComponent', () => {
it('shows loading then content', async () => {
render(AsyncComponent);
// Initially shows loading
await expect.element(page.getByText('Loading...')).toBeVisible();
// Eventually shows content
await expect.element(page.getByText('Data loaded')).toBeVisible();
await expect.element(page.getByText('Loading...')).not.toBeInTheDocument();
});
});
import { vi, beforeEach, afterEach } from 'vitest';
describe('DataFetcher', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('displays fetched data', async () => {
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: async () => ({ name: 'Test User' })
} as Response);
render(DataFetcher);
await expect.element(page.getByText('Test User')).toBeVisible();
});
});
import { createRawSnippet } from 'svelte';
describe('Card', () => {
it('renders children', async () => {
const children = createRawSnippet(() => ({
render: () => '<p>Card content</p>'
}));
render(Card, { props: { children } });
await expect.element(page.getByText('Card content')).toBeVisible();
});
});
For load functions and server code, use Node environment:
// src/routes/api/users.test.ts (note: not .svelte.test.ts)
import { describe, expect, it, vi } from 'vitest';
import { load } from './+page.server';
describe('Users page load', () => {
it('returns user data', async () => {
const result = await load({
params: {},
locals: { user: { id: '1' } }
} as any);
expect(result.users).toBeDefined();
expect(Array.isArray(result.users)).toBe(true);
});
});
# Run all tests
pnpm test
# Watch mode
pnpm test --watch
# Run specific file
pnpm vitest src/lib/components/Button.svelte.test.ts
# Run with UI
pnpm vitest --ui
src/
├── lib/
│ ├── components/
│ │ ├── Button.svelte
│ │ └── Button.svelte.test.ts # Browser test
│ └── utils/
│ ├── format.ts
│ └── format.test.ts # Node test
└── routes/
└── api/
├── +server.ts
└── api.test.ts # Node test
vitest-browser-svelte over jsdomflushSync - When testing runes outside componentsuntrack - To read $state values without trackingActivates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.