← Back to list

svelte-testing-with-sveltest
by maxnoller
Personal collection of Claude Code plugins
⭐ 0🍴 0📅 Jan 17, 2026
SKILL.md
name: Svelte Testing with Sveltest description: 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.
Svelte Testing with Sveltest
Modern 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.
Setup
Install Dependencies
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
Configure Vite
// 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}'],
},
},
],
},
});
Setup File
// src/vitest-setup-client.ts
/// <reference types="vitest/browser" />
/// <reference types="@vitest/browser-playwright" />
Core Testing Patterns
Basic Component Test
// 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);
});
});
Locator Hierarchy
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');
Handling Multiple Elements
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 });
Testing Svelte 5 Runes
Testing $state
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');
});
});
Testing $derived
// 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');
});
});
Testing $effect
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();
});
});
Testing Runes in .svelte.ts Files
// 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);
});
});
Form Testing
Basic Form Test
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();
});
});
Testing with FormData
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');
});
});
Async Testing
Testing Loading States
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();
});
});
Mocking Fetch
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();
});
});
Testing Snippets and Children
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();
});
});
Server-Side Testing
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);
});
});
Running Tests
# 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
File Naming Convention
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
Key Principles
- Use real browsers -
vitest-browser-svelteover jsdom - Prefer semantic queries - roles > labels > text > test IDs
- Test behavior, not implementation - What users see and do
- Use
flushSync- When testing runes outside components - Use
untrack- To read$statevalues without tracking - Separate client/server tests - Different file extensions
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon