0b76a4b119
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test) - Delete old documentation, Docker files, and k8s configs - Add comprehensive Svelte 5 reference library (svelte-reference/) - Update ModeSwitch orchestrator: - Fix port to 3005 (never use 3000/3001) - Add API endpoints for mode switching - Add bun-types and proper tsconfig - Clean up mode folder mappings
16 KiB
16 KiB
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
# 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
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
// 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
// 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)
// 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
// 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
// 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
// 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
// src/lib/stores/users.svelte.ts
export class UsersStore {
users = $state<User[]>([]);
loading = $state(false);
error = $state<string | null>(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
// 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
// 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
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
// 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
// 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
{
"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