# Svelte Testing Guide ## Overview Testing Svelte applications involves unit testing components, integration testing with stores/context, and end-to-end testing with real browsers. --- ## ๐Ÿ”ง Testing Setup ### Install Dependencies ```bash # Vitest for unit/integration tests npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom # Playwright for E2E tests npm install -D @playwright/test npx playwright install ``` ### vitest.config.ts ```typescript import { defineConfig } from 'vitest/config'; import { svelte } from '@sveltejs/vite-plugin-svelte'; export default defineConfig({ plugins: [svelte({ hot: !process.env.VITEST })], test: { include: ['src/**/*.{test,spec}.{js,ts}'], globals: true, environment: 'jsdom', setupFiles: ['./src/tests/setup.ts'], coverage: { reporter: ['text', 'json', 'html'], exclude: ['node_modules/', 'src/tests/'] } } }); ``` ### Test Setup File ```typescript // src/tests/setup.ts import '@testing-library/jest-dom/vitest'; import { vi } from 'vitest'; // Mock SvelteKit modules vi.mock('$app/navigation', () => ({ goto: vi.fn(), invalidate: vi.fn(), invalidateAll: vi.fn(), preloadData: vi.fn(), preloadCode: vi.fn() })); vi.mock('$app/stores', () => ({ page: { subscribe: vi.fn() }, navigating: { subscribe: vi.fn() } })); vi.mock('$app/environment', () => ({ browser: true, dev: true, building: false })); ``` --- ## ๐Ÿงช Unit Testing Components ### Basic Component Test ```typescript // src/lib/components/Button.test.ts import { render, screen, fireEvent } from '@testing-library/svelte'; import { describe, it, expect, vi } from 'vitest'; import Button from './Button.svelte'; describe('Button', () => { it('renders with text', () => { render(Button, { props: { children: 'Click me' } }); expect(screen.getByRole('button')).toHaveTextContent('Click me'); }); it('applies variant class', () => { render(Button, { props: { variant: 'primary', children: 'Primary' } }); expect(screen.getByRole('button')).toHaveClass('btn-primary'); }); it('handles click events', async () => { const onclick = vi.fn(); render(Button, { props: { onclick, children: 'Click' } }); await fireEvent.click(screen.getByRole('button')); expect(onclick).toHaveBeenCalledOnce(); }); it('is disabled when loading', () => { render(Button, { props: { loading: true, children: 'Submit' } }); expect(screen.getByRole('button')).toBeDisabled(); }); }); ``` ### Testing with Snippets (Svelte 5) ```typescript // src/lib/components/Card.test.ts import { render, screen } from '@testing-library/svelte'; import { describe, it, expect } from 'vitest'; import Card from './Card.svelte'; // Helper to create snippet-like content const createCardWithContent = (title: string, content: string) => { // For Svelte 5, you may need a wrapper component return { component: Card, props: { title }, // Slot content for testing slots: { default: content } }; }; describe('Card', () => { it('renders title and content', () => { render(Card, { props: { title: 'Card Title' } // Note: Testing snippets may require wrapper components }); expect(screen.getByText('Card Title')).toBeInTheDocument(); }); }); ``` ### Testing Reactive State ```typescript // src/lib/components/Counter.test.ts import { render, screen, fireEvent } from '@testing-library/svelte'; import { describe, it, expect } from 'vitest'; import Counter from './Counter.svelte'; describe('Counter', () => { it('increments count on click', async () => { render(Counter); const button = screen.getByRole('button', { name: /increment/i }); const display = screen.getByTestId('count'); expect(display).toHaveTextContent('0'); await fireEvent.click(button); expect(display).toHaveTextContent('1'); await fireEvent.click(button); await fireEvent.click(button); expect(display).toHaveTextContent('3'); }); it('starts with initial value', () => { render(Counter, { props: { initialValue: 10 } }); expect(screen.getByTestId('count')).toHaveTextContent('10'); }); }); ``` ### Testing Async Components ```typescript // src/lib/components/UserProfile.test.ts import { render, screen, waitFor } from '@testing-library/svelte'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import UserProfile from './UserProfile.svelte'; // Mock fetch const mockFetch = vi.fn(); global.fetch = mockFetch; describe('UserProfile', () => { beforeEach(() => { mockFetch.mockReset(); }); it('shows loading state', () => { mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves render(UserProfile, { props: { userId: '1' } }); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); it('displays user data after loading', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ name: 'John Doe', email: 'john@example.com' }) }); render(UserProfile, { props: { userId: '1' } }); await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); }); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); it('shows error on fetch failure', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')); render(UserProfile, { props: { userId: '1' } }); await waitFor(() => { expect(screen.getByRole('alert')).toHaveTextContent('Failed to load user'); }); }); }); ``` --- ## ๐Ÿช Testing Stores ### Testing .svelte.ts Stores ```typescript // src/lib/stores/counter.svelte.ts export class Counter { count = $state(0); doubled = $derived(this.count * 2); increment() { this.count++; } decrement() { this.count--; } reset() { this.count = 0; } } // src/lib/stores/counter.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { Counter } from './counter.svelte'; describe('Counter Store', () => { let counter: Counter; beforeEach(() => { counter = new Counter(); }); it('starts at 0', () => { expect(counter.count).toBe(0); }); it('increments', () => { counter.increment(); expect(counter.count).toBe(1); counter.increment(); counter.increment(); expect(counter.count).toBe(3); }); it('decrements', () => { counter.count = 5; counter.decrement(); expect(counter.count).toBe(4); }); it('computes doubled value', () => { counter.count = 5; expect(counter.doubled).toBe(10); counter.increment(); expect(counter.doubled).toBe(12); }); it('resets to 0', () => { counter.count = 100; counter.reset(); expect(counter.count).toBe(0); }); }); ``` ### Testing Async Stores ```typescript // src/lib/stores/users.svelte.ts export class UsersStore { users = $state([]); loading = $state(false); error = $state(null); async fetchUsers() { this.loading = true; this.error = null; try { const response = await fetch('/api/users'); if (!response.ok) throw new Error('Failed to fetch'); this.users = await response.json(); } catch (e) { this.error = e instanceof Error ? e.message : 'Unknown error'; } finally { this.loading = false; } } } // src/lib/stores/users.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { UsersStore } from './users.svelte'; const mockFetch = vi.fn(); global.fetch = mockFetch; describe('UsersStore', () => { let store: UsersStore; beforeEach(() => { store = new UsersStore(); mockFetch.mockReset(); }); it('fetches users successfully', async () => { const mockUsers = [ { id: '1', name: 'John' }, { id: '2', name: 'Jane' } ]; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockUsers) }); await store.fetchUsers(); expect(store.users).toEqual(mockUsers); expect(store.loading).toBe(false); expect(store.error).toBeNull(); }); it('handles fetch error', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500 }); await store.fetchUsers(); expect(store.users).toEqual([]); expect(store.error).toBe('Failed to fetch'); }); it('sets loading state during fetch', async () => { mockFetch.mockImplementation(() => new Promise(resolve => { setTimeout(() => resolve({ ok: true, json: () => Promise.resolve([]) }), 100); })); const promise = store.fetchUsers(); expect(store.loading).toBe(true); await promise; expect(store.loading).toBe(false); }); }); ``` --- ## ๐Ÿ”— Integration Testing ### Testing Components with Context ```typescript // src/lib/components/ThemeConsumer.test.ts import { render, screen } from '@testing-library/svelte'; import { describe, it, expect } from 'vitest'; import ThemeProvider from './ThemeProvider.svelte'; import ThemeConsumer from './ThemeConsumer.svelte'; // Wrapper component for testing import TestWrapper from './TestWrapper.svelte'; describe('ThemeConsumer', () => { it('uses theme from provider', () => { // TestWrapper.svelte wraps children in ThemeProvider render(TestWrapper, { props: { component: ThemeConsumer, theme: 'dark' } }); expect(screen.getByTestId('theme-display')).toHaveTextContent('dark'); }); }); ``` ### Testing Form Submissions ```typescript // src/lib/components/LoginForm.test.ts import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; import { describe, it, expect, vi } from 'vitest'; import LoginForm from './LoginForm.svelte'; describe('LoginForm', () => { it('submits form with credentials', async () => { const onsubmit = vi.fn(); render(LoginForm, { props: { onsubmit } }); await fireEvent.input( screen.getByLabelText('Email'), { target: { value: 'test@example.com' } } ); await fireEvent.input( screen.getByLabelText('Password'), { target: { value: 'password123' } } ); await fireEvent.click(screen.getByRole('button', { name: /login/i })); expect(onsubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123' }); }); it('shows validation errors', async () => { render(LoginForm); await fireEvent.click(screen.getByRole('button', { name: /login/i })); expect(screen.getByText('Email is required')).toBeInTheDocument(); expect(screen.getByText('Password is required')).toBeInTheDocument(); }); it('disables button while loading', async () => { render(LoginForm, { props: { loading: true } }); expect(screen.getByRole('button')).toBeDisabled(); }); }); ``` --- ## ๐ŸŒ E2E Testing with Playwright ### playwright.config.ts ```typescript import type { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { webServer: { command: 'npm run build && npm run preview', port: 4173 }, testDir: 'tests', testMatch: /(.+\.)?(test|spec)\.[jt]s/, use: { baseURL: 'http://localhost:4173', trace: 'on-first-retry', screenshot: 'only-on-failure' }, projects: [ { name: 'chromium', use: { browserName: 'chromium' } }, { name: 'firefox', use: { browserName: 'firefox' } }, { name: 'webkit', use: { browserName: 'webkit' } } ] }; export default config; ``` ### E2E Test Examples ```typescript // tests/auth.test.ts import { test, expect } from '@playwright/test'; test.describe('Authentication', () => { test('user can log in', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]'); // Should redirect to dashboard await expect(page).toHaveURL('/dashboard'); await expect(page.locator('h1')).toContainText('Dashboard'); }); test('shows error for invalid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', 'wrong@example.com'); await page.fill('input[name="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials'); await expect(page).toHaveURL('/login'); }); test('protected routes redirect to login', async ({ page }) => { await page.goto('/dashboard'); await expect(page).toHaveURL('/login'); }); }); // tests/navigation.test.ts test.describe('Navigation', () => { test('main navigation works', async ({ page }) => { await page.goto('/'); await page.click('a[href="/about"]'); await expect(page).toHaveURL('/about'); await expect(page.locator('h1')).toContainText('About'); await page.click('a[href="/contact"]'); await expect(page).toHaveURL('/contact'); await expect(page.locator('h1')).toContainText('Contact'); }); test('back button works', async ({ page }) => { await page.goto('/'); await page.click('a[href="/about"]'); await page.goBack(); await expect(page).toHaveURL('/'); }); }); // tests/forms.test.ts test.describe('Contact Form', () => { test('submits form successfully', async ({ page }) => { await page.goto('/contact'); await page.fill('input[name="name"]', 'John Doe'); await page.fill('input[name="email"]', 'john@example.com'); await page.fill('textarea[name="message"]', 'Hello, this is a test message'); await page.click('button[type="submit"]'); await expect(page.locator('.success-message')).toBeVisible(); await expect(page.locator('.success-message')).toContainText('Message sent'); }); }); ``` ### Testing Accessibility with Playwright ```typescript // tests/accessibility.test.ts import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test.describe('Accessibility', () => { test('home page passes axe', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); }); test('login page passes axe', async ({ page }) => { await page.goto('/login'); const results = await new AxeBuilder({ page }) .exclude('.third-party-widget') // Exclude elements you can't control .analyze(); expect(results.violations).toEqual([]); }); test('keyboard navigation works', async ({ page }) => { await page.goto('/'); // Tab through focusable elements await page.keyboard.press('Tab'); await expect(page.locator(':focus')).toHaveAttribute('href', '/'); await page.keyboard.press('Tab'); await expect(page.locator(':focus')).toHaveText('About'); // Can activate with Enter await page.keyboard.press('Enter'); await expect(page).toHaveURL('/about'); }); }); ``` --- ## ๐Ÿ“‹ Package.json Scripts ```json { "scripts": { "test": "vitest", "test:unit": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:all": "npm run test:unit && npm run test:e2e" } } ``` --- ## ๐Ÿ“‹ Testing Checklist ### Unit Tests - [ ] All components have tests - [ ] Props and variants tested - [ ] Event handlers tested - [ ] Edge cases covered - [ ] Async behavior tested ### Integration Tests - [ ] Store interactions tested - [ ] Context providers tested - [ ] Form submissions tested - [ ] API calls mocked ### E2E Tests - [ ] Critical user flows covered - [ ] Authentication tested - [ ] Form submissions tested - [ ] Error states tested - [ ] Accessibility automated