Testing Svelte Components: From Unit to E2E Tests
Testing is a critical part of building robust Svelte applications. Whether you're building a small widget or a large-scale application, having a comprehensive testing strategy ensures your components behave as expected, regressions are caught early, and refactoring becomes safer. In this tutorial, we'll explore the full testing spectrum for Svelte components — from isolated unit tests to end-to-end (E2E) tests that simulate real user interactions.
Why Testing Svelte Components Matters
Svelte's compiler-based approach makes it incredibly easy to build reactive, performant UIs. However, this same reactivity can introduce subtle bugs that are hard to catch manually. For example, a store update might trigger unexpected side effects, or a component might not clean up its event listeners properly. Automated tests help you catch these issues before they reach production.
Here are the key benefits of testing Svelte components:
- Confidence in refactoring: When you change internal logic, tests verify that external behavior remains intact.
- Documentation: Tests serve as living documentation showing how components should behave.
- Faster feedback loops: Unit tests run in milliseconds, giving you instant feedback during development.
- Reduced regression bugs: Automated tests catch issues that manual testing might miss.
- Better component design: Writing tests first (TDD) encourages simpler, more focused components.
The Testing Pyramid for Svelte
Before diving into code, it's important to understand the testing pyramid and how it applies to Svelte applications:
- Unit tests: Test individual functions, stores, or isolated component logic.
- Component tests: Test a single component's rendering, props, events, and user interactions.
- Integration tests: Test how multiple components work together.
- E2E tests: Test full user flows through a real browser, simulating actual user behavior.
You should have many unit and component tests, fewer integration tests, and a small number of E2E tests. This balance keeps your test suite fast while still providing comprehensive coverage.
Setting Up the Testing Environment
For modern Svelte testing, the recommended stack is Vitest for unit and component testing, @testing-library/svelte for component rendering and interaction, and Playwright for E2E testing. Let's set up the environment step by step.
Installing Dependencies
If you're using a Vite-based Svelte project (created with npm create svelte@latest), you can install the testing dependencies as follows:
npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
npm install -D @playwright/test
npx playwright install
Configuring Vitest
Create or update your vite.config.js file to include Vitest configuration:
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.js'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
Creating the Test Setup File
Create a tests/setup.js file to configure custom matchers from @testing-library/jest-dom:
import '@testing-library/jest-dom/vitest';
This setup gives you access to helpful DOM matchers like toBeInTheDocument(), toHaveTextContent(), and toBeVisible() in your tests.
Unit Testing Svelte Stores and Logic
Unit tests focus on testing individual pieces of logic in isolation. In Svelte applications, this often means testing stores and utility functions. Let's start with a simple example.
Testing a Svelte Store
Suppose you have a counter store defined in src/stores/counter.js:
import { writable } from 'svelte/store';
export function createCounter(initialValue = 0) {
const { subscribe, set, update } = writable(initialValue);
return {
subscribe,
increment: () => update((n) => n + 1),
decrement: () => update((n) => n - 1),
reset: () => set(initialValue),
};
}
Here's how you would write unit tests for this store:
import { describe, it, expect } from 'vitest';
import { get } from 'svelte/store';
import { createCounter } from '../src/stores/counter';
describe('createCounter', () => {
it('initializes with the given value', () => {
const counter = createCounter(5);
expect(get(counter)).toBe(5);
});
it('increments the value', () => {
const counter = createCounter(0);
counter.increment();
expect(get(counter)).toBe(1);
});
it('decrements the value', () => {
const counter = createCounter(5);
counter.decrement();
expect(get(counter)).toBe(4);
});
it('resets to the initial value', () => {
const counter = createCounter(10);
counter.increment();
counter.increment();
counter.reset();
expect(get(counter)).toBe(10);
});
});
These tests are fast, isolated, and verify the core logic of the store without any DOM involvement.
Testing Utility Functions
Utility functions are the easiest to unit test. For example, if you have a formatting utility:
// src/utils/format.js
export function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount);
}
export function truncate(text, maxLength = 50) {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - 3) + '...';
}
The corresponding test file would be:
import { describe, it, expect } from 'vitest';
import { formatCurrency, truncate } from '../src/utils/format';
describe('formatCurrency', () => {
it('formats a number as USD by default', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
});
it('formats a number with a custom currency', () => {
expect(formatCurrency(1234.56, 'EUR')).toBe('€1,234.56');
});
it('handles zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
});
describe('truncate', () => {
it('returns the original text if shorter than max length', () => {
expect(truncate('Hello', 50)).toBe('Hello');
});
it('truncates long text with ellipsis', () => {
const longText = 'This is a very long text that needs to be truncated';
expect(truncate(longText, 20)).toBe('This is a very lo...');
});
});
Component Testing with @testing-library/svelte
Component testing is where things get interesting. @testing-library/svelte allows you to render Svelte components in a simulated DOM environment and interact with them as a user would. The library's guiding principle is: "The more your tests resemble the way your software is used, the more confidence they can give you."
Testing a Simple Component
Let's start with a simple Button component:
<!-- src/lib/Button.svelte -->
<script>
export let label = 'Click me';
export let disabled = false;
export let variant = 'primary';
function handleClick(event) {
if (disabled) return;
dispatch('click', event);
}
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<button
class="btn btn-{variant}"
{disabled}
on:click={handleClick}
>
{label}
</button>
Here's how you would test this component:
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import Button from '../src/lib/Button.svelte';
describe('Button', () => {
it('renders with default label', () => {
const { getByRole } = render(Button);
expect(getByRole('button')).toHaveTextContent('Click me');
});
it('renders with a custom label', () => {
const { getByRole } = render(Button, { props: { label: 'Submit' } });
expect(getByRole('button')).toHaveTextContent('Submit');
});
it('applies the correct variant class', () => {
const { getByRole } = render(Button, { props: { variant: 'danger' } });
expect(getByRole('button')).toHaveClass('btn-danger');
});
it('dispatches a click event when clicked', async () => {
const { getByRole, component } = render(Button);
const mockHandler = vi.fn();
component.$on('click', mockHandler);
await fireEvent.click(getByRole('button'));
expect(mockHandler).toHaveBeenCalledTimes(1);
});
it('does not dispatch click when disabled', async () => {
const { getByRole, component } = render(Button, {
props: { disabled: true },
});
const mockHandler = vi.fn();
component.$on('click', mockHandler);
await fireEvent.click(getByRole('button'));
expect(mockHandler).not.toHaveBeenCalled();
});
});
Testing Component Props and Reactivity
Svelte's reactivity is one of its most powerful features. Let's test a component that reacts to prop changes. Consider a Greeting component:
<!-- src/lib/Greeting.svelte -->
<script>
export let name = 'World';
export let count = 0;
</script>
<h1>Hello, {name}!</h1>
<p>You have visited {count} {count === 1 ? 'time' : 'times'}.</p>
Testing reactivity involves updating component props and verifying the DOM updates:
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import Greeting from '../src/lib/Greeting.svelte';
describe('Greeting', () => {
it('renders with default props', () => {
const { getByText } = render(Greeting);
expect(getByText('Hello, World!')).toBeInTheDocument();
expect(getByText('You have visited 0 times.')).toBeInTheDocument();
});
it('updates when props change', async () => {
const { getByText, rerender } = render(Greeting, {
props: { name: 'Alice', count: 1 },
});
expect(getByText('Hello, Alice!')).toBeInTheDocument();
expect(getByText('You have visited 1 time.')).toBeInTheDocument();
await rerender({ props: { name: 'Bob', count: 5 } });
expect(getByText('Hello, Bob!')).toBeInTheDocument();
expect(getByText('You have visited 5 times.')).toBeInTheDocument();
});
});
Testing Components with Slots
Testing components that use slots requires passing slot content during rendering. Here's a Card component:
<!-- src/lib/Card.svelte -->
<script>
export let title = '';
</script>
<div class="card">
{#if title}
<h2 class="card-title">{title}</h2>
{/if}
<div class="card-body">
<slot />
</div>
</div>
To test the slot, you can use the props option with a special $$slots approach or wrap the component:
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import Card from '../src/lib/Card.svelte';
describe('Card', () => {
it('renders title and slot content', () => {
const { getByText, container } = render(Card, {
props: { title: 'My Card' },
});
expect(getByText('My Card')).toBeInTheDocument();
expect(container.querySelector('.card-body')).toBeInTheDocument();
});
it('does not render title when not provided', () => {
const { container } = render(Card);
expect(container.querySelector('.card-title')).not.toBeInTheDocument();
});
});
Testing Components with Stores
Many Svelte components rely on stores for state management. Testing these components requires careful handling of store state. Let's look at a TodoList component that uses a store:
<!-- src/lib/TodoList.svelte -->
<script>
import { todos, addTodo, toggleTodo, removeTodo } from '../stores/todos.js';
let newTodoText = '';
function handleSubmit(event) {
event.preventDefault();
if (newTodoText.trim()) {
addTodo(newTodoText.trim());
newTodoText = '';
}
}
</script>
<form on:submit={handleSubmit}>
<input type="text" bind:value={newTodoText} placeholder="Add a todo..." />
<button type="submit">Add</button>
</form>
<ul>
{#each $todos as todo (todo.id)}
<li class={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
on:change={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button on:click={() => removeTodo(todo.id)}>Delete</button>
</li>
{/each}
</ul>
The store definition:
// src/stores/todos.js
import { writable } from 'svelte/store';
function createTodoStore() {
const { subscribe, update } = writable([]);
return {
subscribe,
addTodo: (text) => update((todos) => [
...todos,
{ id: Date.now(), text, completed: false },
]),
toggleTodo: (id) => update((todos) =>
todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
),
removeTodo: (id) => update((todos) =>
todos.filter((t) => t.id !== id)
),
};
}
export const todos = createTodoStore();
export const { addTodo, toggleTodo, removeTodo } = todos;
Testing the component with the store:
import { describe, it, expect, beforeEach } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import TodoList from '../src/lib/TodoList.svelte';
import { todos, addTodo } from '../src/stores/todos.js';
import { get } from 'svelte/store';
describe('TodoList', () => {
beforeEach(() => {
// Reset the store before each test
todos.set([]);
});
it('renders an empty list initially', () => {
const { container } = render(TodoList);
expect(container.querySelector('ul').children).toHaveLength(0);
});
it('adds a new todo when form is submitted', async () => {
const { getByPlaceholderText, getByText, container } = render(TodoList);
const input = getByPlaceholderText('Add a todo...');
await fireEvent.input(input, { target: { value: 'Buy groceries' } });
await fireEvent.click(getByText('Add'));
expect(container.querySelector('ul').children).toHaveLength(1);
expect(getByText('Buy groceries')).toBeInTheDocument();
});
it('toggles todo completion when checkbox is clicked', async () => {
addTodo('Test todo');
const { getByRole, container } = render(TodoList);
const checkbox = getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
expect(container.querySelector('li')).toHaveClass('completed');
});
it('removes a todo when delete button is clicked', async () => {
addTodo('Todo to delete');
const { getByText, container } = render(TodoList);
await fireEvent.click(getByText('Delete'));
expect(container.querySelector('ul').children).toHaveLength(0);
});
});
Mocking Dependencies in Component Tests
Sometimes your components depend on external modules, API calls, or other components. Vitest provides powerful mocking capabilities. Here's an example of mocking an API call:
// src/lib/UserProfile.svelte
<script>
import { onMount } from 'svelte';
import { fetchUser } from '../api/users.js';
let user = null;
let loading = true;
let error = null;
onMount(async () => {
try {
user = await fetchUser(1);
} catch (e) {
error = e.message;
} finally {
loading = false;
}
});
</script>
{#if loading}
<p data-testid="loading">Loading...</p>
{:else if error}
<p data-testid="error">Error: {error}</p>
{:else if user}
<div data-testid="profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
{/if}
Testing with a mocked API:
import { describe, it, expect, vi } from 'vitest';
import { render, waitFor } from '@testing-library/svelte';
import UserProfile from '../src/lib/UserProfile.svelte';
vi.mock('../src/api/users.js', () => ({
fetchUser: vi.fn(),
}));
import { fetchUser } from '../src/api/users.js';
describe('UserProfile', () => {
it('displays user data when API call succeeds', async () => {
fetchUser.mockResolvedValue({
id: 1,
name: 'Jane Doe',
email: 'jane@example.com',
});
const { getByTestId, getByText } = render(UserProfile);
expect(getByTestId('loading')).toBeInTheDocument();
await waitFor(() => {
expect(getByTestId('profile')).toBeInTheDocument();
});
expect(getByText('Jane Doe')).toBeInTheDocument();
expect(getByText('jane@example.com')).toBeInTheDocument();
});
it('displays error message when API call fails', async () => {
fetchUser.mockRejectedValue(new Error('Network error'));
const { getByTestId } = render(UserProfile);
await waitFor(() => {
expect(getByTestId('error')).toBeInTheDocument();
});
expect(getByTestId('error')).toHaveTextContent('Error: Network error');
});
});
Testing Svelte Components with Lifecycle Hooks
Svelte components have lifecycle hooks like onMount, onDestroy, and tick. Testing these requires understanding when they fire. Here's an example component that sets up an interval on mount:
<!-- src/lib/Timer.svelte -->
<script>
import { onMount, onDestroy } from 'svelte';
let seconds = 0;
let interval;
onMount(() => {
interval = setInterval(() => {
seconds += 1;
}, 1000);
});
onDestroy(() => {
if (interval) clearInterval(interval);
});
</script>
<p data-testid="timer">{seconds} seconds elapsed</p>
Testing with fake timers:
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render } from '@testing-library/svelte';
import Timer from '../src/lib/Timer.svelte';
describe('Timer', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('starts at zero', () => {
const { getByTestId } = render(Timer);
expect(getByTestId('timer')).toHaveTextContent('0 seconds elapsed');
});
it('increments every second', async () => {
const { getByTestId } = render(Timer);
vi.advanceTimersByTime(3000);
// Wait for Svelte's reactivity to flush
await new Promise((resolve) => setTimeout(resolve, 0));
expect(getByTestId('timer')).toHaveTextContent('3 seconds elapsed');
});
});
Integration Testing Multiple Components
Integration tests verify that multiple components work together correctly. Let's test a small application that combines several components. Consider a shopping cart feature:
<!-- src/lib/ShoppingCart.svelte -->
<script>
import ProductList from './ProductList.svelte';
import CartSummary from './CartSummary.svelte';
import { cart } from '../stores/cart.js';
function handleAddToCart(product) {
cart.addItem(product);
}
</script>
<div class="shopping-cart">
<ProductList on:addtocart={(e) => handleAddToCart(e.detail)} />
<CartSummary />
</div>
Testing the integration:
import { describe, it, expect, beforeEach } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import ShoppingCart from '../src/lib/ShoppingCart.svelte';
import { cart } from '../src/stores/cart.js';
describe('ShoppingCart integration', () => {
beforeEach(() => {
cart.clear();
});
it('adds products to cart and updates summary', async () => {
const { getByText, getByTestId } = render(ShoppingCart);
// Initially cart is empty
expect(getByTestId('cart-count')).toHaveTextContent('0');
expect(getByTestId('cart-total')).toHaveTextContent('$0.00');
// Add a product
await fireEvent.click(getByText('Add to Cart'));
// Cart should update
expect(getByTestId('cart-count')).toHaveTextContent('1');
expect(getByTestId('cart-total')).toHaveTextContent('$29.99');
});
it('adds multiple products and calculates total', async () => {
const { getAllByText, getByTestId } = render(ShoppingCart);
const addButtons = getAllByText('Add to Cart');
await fireEvent.click(addButtons[0]);
await fireEvent.click(addButtons[1]);
expect(getByTestId('cart-count')).toHaveTextContent('2');
});
});
End-to-End Testing with Playwright
While unit and component tests verify individual pieces, E2E tests verify entire user flows in a real browser. Playwright is the recommended tool for E2E testing Svelte applications because it's fast, reliable, and has excellent developer experience.
Configuring Playwright
Create a playwright.config.js file:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
Writing Your First E2E Test
Let's write an E2E test for a login flow. Assume your Svelte app has a login page at /login:
// tests/e2e/login.spec.js
import { test, expect } from '@playwright/test';
test.describe('Login flow', () => {
test('successfully logs in with valid credentials', async ({ page }) => {
await page.goto('/login');
// Fill in the login form
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
// Submit the form
await page.click('[data-testid="login-button"]');
// Verify redirect to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toContainText(
'Welcome back'
);
});
test('shows error with invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'wrong@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(
'Invalid credentials'
);
});
test('validates required fields', async ({ page }) => {
await page.goto('/login');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
await expect(page.locator('[data-testid="password-error"]')).toBeVisible();
});
});
Testing a Complete Shopping Flow
Here's a more complex E2E test that covers a full shopping experience:
// tests/e2e/shopping.spec.js
import { test, expect } from '@playwright/test';
test.describe('Shopping flow', () => {
test('browse, add to cart, and checkout', async ({ page }) => {
// Navigate to products page
await page.goto('/products');
// Wait for products to load
await expect(page.locator('[data-testid="product-card"]')).toHaveCount(6);
// Add first product to cart
await page.locator('[data-testid="product-card"]').first()
.locator('button:has-text("Add to Cart")').click();
// Verify cart badge updates
await expect(page.locator('[data-testid="cart-badge"]')).toHaveText('1');
// Go to cart page
await page.click('[data-testid="cart-link"]');
// Verify product is in cart
await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(1);
// Proceed to checkout
await page.click('[data-testid="checkout-button"]');
// Fill in shipping information
await page.fill('[data-testid="name-input"]', 'John Doe');
await page.fill('[data-testid="address-input"]', '123 Main St');
await page.fill('[data-testid="city-input"]', 'Anytown');
await page.fill('[data-testid="zip-input"]', '12345');
// Place order
await page.click('[data-testid="place-order-button"]');
// Verify order confirmation
await expect(page).toHaveURL('/order-confirmation');
await expect(page.locator('[data-testid="order-number"]')).toBeVisible();
});
test('can remove items from cart', async ({ page }) => {
await page.goto('/products');
// Add two products
await page.locator('[data-testid="product-card"]').nth(0)
.locator('button:has-text("Add to Cart")').click();
await page.locator('[data-testid="product-card"]').nth(1)
.locator('button:has-text("Add to Cart")').click();
// Go to cart
await page.click('[data-testid="cart-link"]');
// Remove first item
await page.locator('[data-testid="cart-item"]').first()
.locator('[data-testid="remove-item"]').click();
// Verify only one item remains
await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(1);
});
});
Testing SvelteKit Pages
If you're using SvelteKit, you'll want to test page routing and server-side features. Playwright handles this naturally since it tests against a running server:
// tests/e2e/navigation.spec.js
import { test, expect } from '@playwright/test';
test.describe('Navigation', () => {
test('can navigate between pages using links', async ({ page }) => {
await page.goto('/');
// Click on the About link
await page.click('a:has-text("About")');
await expect(page).toHaveURL('/about');
await expect(page.locator('h1')).toHaveText('About Us');
// Navigate to Contact
await page.click('a:has-text("Contact")');
await expect(page).toHaveURL('/contact');
});
test('handles 404 for unknown routes', async ({ page }) => {
await page.goto('/nonexistent-page');
await expect(page.locator('[data-testid="not-found"]')).toBeVisible();
});
test('preserves state when navigating back', async ({ page }) => {
await page.goto('/search');
await page.fill('[data-testid="search-input"]', 'svelte testing');
await page.click('[data-testid="search-button"]');
// Navigate away
await page.click('a:has-text("Home")');
// Navigate back
await page.goBack();
// Search input should still have the value (if using SvelteKit's state preservation)
await expect(page.locator('[data-testid="search-input"]')).toHaveValue(
'svelte testing'
);
});
});
Testing Svelte Transitions and Animations
Svelte's transition directives can be tricky to test because they involve CSS animations. The key is to test the state before and after the transition rather than the animation itself:
<!-- src/lib/Modal.svelte -->
<script>
import { fade } from 'svelte/transition';
export let open = false;
</script>
{#if open}
<div class="modal-overlay" transition:fade on:click|self={() => (open = false)}>
<div class="modal-content" data-testid="modal-content">
<slot />
<button data-testid="modal-close" on:click={() => (open = false)}>
Close
</button>
</div>
</div>
{/if}
Testing the modal:
import { describe, it, expect } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import Modal from '../src/lib/Modal.svelte';
describe('Modal', () => {
it('does not render when open is false', () => {
const { queryByTestId } = render(Modal, { props: { open: false } });
expect(queryByTestId('modal-content')).not.toBeInTheDocument();
});
it('renders when open is true', () => {
const { getByTestId } = render(Modal, { props: { open: true } });
expect(getByTestId('modal-content')).toBeInTheDocument();
});
it('closes when close button is clicked', async () => {
const { getByTestId, queryByTestId, component } = render(Modal, {
props: { open: true },
});
await fireEvent.click(getByTestId('modal-close'));
// The component's open prop should be updated
expect(component.open).toBe(false);
});
});
Best Practices for Testing Svelte Components
1. Test Behavior, Not Implementation
Avoid testing internal implementation details that are likely to change. Instead, focus on what the component does from the user's perspective. Use semantic queries like getByRole, getByLabelText, and getByText rather than querying by class names or DOM structure.
// ❌ Bad: Testing implementation details
expect(component.querySelector('.internal-class-name')).toBeTruthy();
// ✅ Good: Testing user-visible behavior
expect(getByRole('button', { name: 'Submit' })).toBeVisible();
2. Use data-testid for Unstable Elements
When text content or roles are dynamic or ambiguous, use data-testid attributes to create stable selectors:
<button data-testid="submit-order">Place Order</button>
3. Keep Tests Isolated
Each test should be independent and not rely on the state from previous tests. Use beforeEach to reset stores and mock state:
beforeEach(() => {
cart.clear();
vi.clearAllMocks();
});
4. Test Accessibility
Include accessibility checks in your tests. This ensures your components are usable by everyone:
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import FormField from '../src/lib/FormField.svelte';
describe('FormField accessibility', () => {
it('associates label with input', () => {
const { getByLabelText } = render(FormField, {
props: { label: 'Email', id: 'email' },
});
const input = getByLabelText('Email');
expect(input).toHaveAttribute('type', 'email');
});
it('marks required fields with aria-required', () => {
const { getByLabelText } = render(FormField, {
props: { label: 'Name', id: 'name', required: true },
});
expect(getByLabelText('Name')).toHaveAttribute('aria