
Component Testing Patterns
- 4 installs
- 6 repo stars
- Updated August 3, 2026
- spences10/devhub-crm
Helps with testing & qa tasks.
About
component-testing-patterns is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted development.
- component-testing-patterns
- Testing & QA
- AI-coding skill
Component Testing Patterns by the numbers
- 4 all-time installs (skills.sh)
- Ranked #1,631 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/devhub-crm --skill component-testing-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 6 |
| Last updated | August 3, 2026 |
| Repository | spences10/devhub-crm ↗ |
What it does
Helps with testing & qa tasks.
Files
Component Testing Patterns
Quick Start
import { page } from 'vitest/browser';
import { render } from 'vitest-browser-svelte';
render(Button, { label: 'Click' });
await page.getByRole('button', { name: 'Click' }).click();
await expect.element(page.getByRole('button')).toBeInTheDocument();Core Principles
- Locators, never containers:
page.getByRole()auto-retries - Semantic queries:
getByRole(),getByLabelText()for
accessibility
- Await assertions:
await expect.element(el).toBeInTheDocument() - Real browsers: Tests run in Playwright, not jsdom
Common Patterns
- Locators:
page.getByRole('button'),.first(),.nth(0),
.last()
- Interactions:
await input.fill('text'),await button.click() - Runes: Use
.test.svelte.tsfiles,flushSync(),untrack() - Files:
*.svelte.test.ts(browser),*.ssr.test.ts(SSR),
*.test.ts (server)
References
- setup-configuration.md -
Complete Vitest browser setup
- testing-patterns.md -
Comprehensive testing patterns
- locator-strategies.md - Semantic
locator guide
- troubleshooting.md - Common issues
and fixes
<!-- PROGRESSIVE DISCLOSURE GUIDELINES:
- Keep this file ~50 lines total (max ~150 lines)
- Use 1-2 code blocks only (recommend 1)
- Keep description <200 chars for Level 1 efficiency
- Move detailed docs to references/ for Level 3 loading
- This is Level 2 - quick reference ONLY, not a manual
-->
Component Testing Patterns
Vitest browser mode component testing. Use for testing Svelte 5 components with real browsers, locators, accessibility patterns, and reactive state.
Structure
SKILL.md- Main skill instructionsreferences/- Detailed documentation loaded as neededscripts/- Executable code for deterministic operationsassets/- Templates, images, or other resources
Usage
This skill is automatically discovered by Claude when relevant to the task.
Locator Strategies
Priority Order
Use locators in this order of preference:
1. Semantic roles - getByRole() 2. Labels - getByLabel() 3. Text content - getByText() 4. Test IDs - getByTestId()
Semantic Roles (Recommended)
Buttons
page.getByRole('button', { name: 'Submit' });
page.getByRole('button', { name: /submit/i }); // Case insensitiveLinks
page.getByRole('link', { name: 'Home' });
page.getByRole('link', { name: 'Contact Us' });Headings
page.getByRole('heading', { name: 'Welcome', level: 1 });
page.getByRole('heading', { level: 2 });Form Elements
page.getByRole('textbox', { name: 'Email' });
page.getByRole('checkbox', { name: 'Accept terms' });
page.getByRole('radio', { name: 'Option 1' });
page.getByRole('combobox', { name: 'Country' });Other Common Roles
page.getByRole('navigation');
page.getByRole('main');
page.getByRole('banner');
page.getByRole('contentinfo');
page.getByRole('dialog');
page.getByRole('alert');
page.getByRole('status');
page.getByRole('list');
page.getByRole('listitem');
page.getByRole('table');
page.getByRole('row');
page.getByRole('cell');By Label (Form Elements)
Best for form inputs with associated labels:
page.getByLabel('Email address');
page.getByLabel('Password');
page.getByLabel('Remember me');Component example:
<label>
Email address
<input type="email" name="email" />
</label>Or with for attribute:
<label for="email">Email address</label>
<input id="email" type="email" name="email" />By Text
For elements containing specific text:
page.getByText('Welcome back');
page.getByText(/welcome/i); // Case insensitive
page.getByText('Error:', { exact: false }); // Partial matchBy Test ID
Last resort when semantic locators aren't available:
page.getByTestId('submit-button');
page.getByTestId('user-profile');Component example:
<button data-testid="submit-button">Submit</button>By Placeholder
For inputs with placeholder text:
page.getByPlaceholder('Enter your email');
page.getByPlaceholder(/search/i);By Alt Text
For images:
page.getByAltText('Company logo');
page.getByAltText(/profile/i);By Title
For elements with title attributes:
page.getByTitle('Close');
page.getByTitle('More information');Combining Locators
Filtering
page.getByRole('button').filter({ hasText: 'Delete' });
page.getByRole('listitem').filter({ has: page.getByRole('button') });Chaining
page.getByRole('navigation').getByRole('link', { name: 'Home' });
page.getByTestId('user-card').getByRole('button', { name: 'Edit' });Handling Multiple Matches
Selectors
page.getByRole('button').first();
page.getByRole('button').last();
page.getByRole('button').nth(1); // Zero-indexedGetting All
const buttons = page.getByRole('button').all();
expect(buttons).toHaveLength(3);State-Based Locators
Disabled
page.getByRole('button', { disabled: true });
page.getByRole('button', { disabled: false });Checked
page.getByRole('checkbox', { checked: true });
page.getByRole('radio', { checked: false });Expanded
page.getByRole('button', { expanded: true });Pressed
page.getByRole('button', { pressed: true });Advanced Patterns
Within Specific Container
const dialog = page.getByRole('dialog');
dialog.getByRole('button', { name: 'Confirm' });By CSS Selector (Avoid)
Only use as last resort:
page.locator('.my-custom-class');
page.locator('#unique-id');
page.locator('[data-custom-attr="value"]');By XPath (Avoid)
page.locator('xpath=//button[contains(text(), "Submit")]');Best Practices
1. Always prefer semantic roles - They match how users and assistive technologies interact 2. Use exact names when possible - More explicit and less fragile 3. Avoid CSS classes - They're implementation details that change 4. Avoid XPath - Hard to read and maintain 5. Use test IDs sparingly - Only when no semantic option exists 6. Test accessibility - If you can't find it with semantic locators, users with screen readers can't either
Common ARIA Roles Reference
| Role | HTML Element | Example |
|---|---|---|
button | <button>, <input type="button"> | Clickable buttons |
link | <a href> | Navigation links |
heading | <h1> to <h6> | Section headings |
textbox | <input type="text">, <textarea> | Text inputs |
checkbox | <input type="checkbox"> | Checkboxes |
radio | <input type="radio"> | Radio buttons |
combobox | <select>, custom dropdowns | Dropdowns |
navigation | <nav> | Navigation areas |
main | <main> | Main content |
banner | <header> | Page header |
contentinfo | <footer> | Page footer |
dialog | Custom dialogs with role="dialog" | Modal dialogs |
alert | Elements with role="alert" | Error/warning messages |
Migration Guide: @testing-library/svelte to vitest-browser-svelte
Why Migrate?
Key Benefits
1. Real Browser Testing: Tests run in actual Playwright browsers instead of jsdom simulation 2. Better Svelte 5 Support: Native compatibility with runes, snippets, and modern patterns 3. Built-in Reliability: Auto-retry logic eliminates flaky tests 4. Official Recommendation: Endorsed by the Svelte team for modern projects 5. Simplified API: No need for manual waitFor patterns
Core Changes
1. Update Dependencies
# Install new packages
pnpm install -D @vitest/browser-playwright vitest-browser-svelte playwright
# Remove old packages
pnpm un @testing-library/jest-dom @testing-library/svelte jsdom2. Import Transformations
Before (@testing-library/svelte)
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';After (vitest-browser-svelte)
import { page } from 'vitest/browser';
import { render } from 'vitest-browser-svelte';3. Query Pattern Changes
Before: Screen Queries
const button = screen.getByRole('button');
const input = screen.getByLabelText('Email');
const text = screen.getByText('Hello');After: Page Locators
const button = page.getByRole('button');
const input = page.getByLabelText('Email');
const text = page.getByText('Hello');4. Assertion Updates
Before: Synchronous Assertions
expect(element).toBeInTheDocument();
expect(button).toBeDisabled();
expect(input).toHaveValue('test');After: Async Element Assertions
await expect.element(element).toBeInTheDocument();
await expect.element(button).toBeDisabled();
await expect.element(input).toHaveValue('test');5. Event Handling Simplification
Before: userEvent API
const user = userEvent.setup();
await user.type(input, 'text');
await user.click(button);
await user.clear(input);
await user.selectOptions(select, 'option');After: Direct Element Methods
await input.fill('text');
await button.click();
await input.clear();
await select.selectOption('option');6. Waiting Patterns
Before: Manual waitFor
import { waitFor } from '@testing-library/svelte';
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});After: Built-in Auto-Retry
// No waitFor needed - locators auto-retry
const loaded = page.getByText('Loaded');
await expect.element(loaded).toBeVisible();Configuration Changes
Before: jsdom Environment
// vite.config.ts
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./vitest-setup.ts'],
},
});After: Browser Mode with Projects
// vite.config.ts
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
projects: [
{
extends: true,
test: {
name: 'client',
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
setupFiles: ['./src/vitest-setup-client.ts'],
},
},
],
},
});Complete Example Migration
Before: @testing-library/svelte
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { describe, expect, it } from 'vitest';
import LoginForm from './login-form.svelte';
describe('LoginForm', () => {
it('submits form with credentials', async () => {
const user = userEvent.setup();
render(LoginForm);
const emailInput = screen.getByLabelText('Email');
const passwordInput = screen.getByLabelText('Password');
const submitButton = screen.getByRole('button', {
name: 'Login',
});
await user.type(emailInput, 'user@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument();
});
});
});After: vitest-browser-svelte
import { page } from 'vitest/browser';
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import LoginForm from './login-form.svelte';
describe('LoginForm', () => {
it('submits form with credentials', async () => {
render(LoginForm);
const emailInput = page.getByLabelText('Email');
const passwordInput = page.getByLabelText('Password');
const submitButton = page.getByRole('button', { name: 'Login' });
await emailInput.fill('user@example.com');
await passwordInput.fill('password123');
await submitButton.click();
const success = page.getByText('Success');
await expect.element(success).toBeInTheDocument();
});
});Common Migration Issues
Issue: screen is not defined
Problem:
const button = screen.getByRole('button');
// ReferenceError: screen is not definedSolution:
const button = page.getByRole('button');Issue: Assertion fails immediately
Problem:
expect(element).toBeInTheDocument();
// Element not foundSolution:
await expect.element(element).toBeInTheDocument();Issue: userEvent not working
Problem:
await user.type(input, 'text');
// userEvent is not definedSolution:
await input.fill('text');Issue: Element role confusion
Problem:
page.getByRole('input'); // FailsSolution:
page.getByRole('textbox'); // Correct role nameFile Renaming
Rename test files to match the new conventions:
component.test.ts→component.svelte.test.ts(browser mode)- Keep
utils.test.tsas is (Node environment)
Step-by-Step Migration Process
1. Update dependencies (install new, remove old) 2. Update vite.config.ts with browser mode configuration 3. Create setup file (vitest-setup-client.ts) 4. Update one test file as a proof of concept 5. Verify it works before proceeding 6. Migrate remaining tests file by file 7. Remove old setup files and jsdom configuration 8. Update CI/CD to install Playwright browsers
Benefits After Migration
- ✅ No more flaky tests due to timing issues
- ✅ Tests run in real browsers (Chromium, Firefox, WebKit)
- ✅ Better Svelte 5 support with runes
- ✅ Simpler API without waitFor patterns
- ✅ Improved accessibility testing
- ✅ Better debugging with browser dev tools
Comprehensive Component Testing Patterns
Essential Imports
import { page } from 'vitest/browser';
import { describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { flushSync, untrack } from 'svelte';Locator Strategies
Semantic Queries (Preferred)
// Role-based (most accessible)
page.getByRole('button', { name: 'Submit' });
page.getByRole('textbox', { name: 'Email' });
page.getByRole('heading', { level: 1 });
// Label-based (forms)
page.getByLabelText('Email');
page.getByLabelText('Password', { exact: true });
// Text content
page.getByText('Welcome');
page.getByText(/hello/i);
// Test IDs (last resort)
page.getByTestId('custom-element');Handling Multiple Matches
When strict mode fails due to multiple elements:
// Get first match
const firstButton = page.getByRole('button').first();
// Get nth match (0-indexed)
const secondButton = page.getByRole('button').nth(1);
// Get last match
const lastButton = page.getByRole('button').last();Component Testing Patterns
Button Component
describe('Button', () => {
it('renders with different variants', async () => {
render(Button, { variant: 'primary', label: 'Click' });
const button = page.getByRole('button', { name: 'Click' });
await expect.element(button).toBeInTheDocument();
await expect.element(button).toHaveClass(/primary/);
});
it('handles click events', async () => {
const handleClick = vi.fn();
render(Button, { onclick: handleClick, label: 'Click' });
const button = page.getByRole('button');
await button.click();
expect(handleClick).toHaveBeenCalledOnce();
});
it('respects disabled state', async () => {
render(Button, { disabled: true, label: 'Submit' });
const button = page.getByRole('button');
await expect.element(button).toBeDisabled();
});
it('clicks animated elements', async () => {
render(Button, { animated: true });
const button = page.getByRole('button');
// Force click through animations
await button.click({ force: true });
});
});Input Component
describe('Input', () => {
it('accepts user input', async () => {
render(Input, { label: 'Email' });
const input = page.getByRole('textbox', { name: 'Email' });
await input.fill('user@example.com');
await expect.element(input).toHaveValue('user@example.com');
});
it('displays validation errors', async () => {
render(Input, { label: 'Email', error: 'Invalid email' });
const error = page.getByText('Invalid email');
await expect.element(error).toBeVisible();
});
it('shows correct input type', async () => {
render(Input, { type: 'password', label: 'Password' });
const input = page.getByLabelText('Password');
await expect.element(input).toHaveAttribute('type', 'password');
});
});Modal Component
describe('Modal', () => {
it('manages focus correctly', async () => {
render(Modal, { open: true, title: 'Confirm' });
const modal = page.getByRole('dialog');
await expect.element(modal).toBeFocused();
});
it('closes on escape key', async () => {
const handleClose = vi.fn();
render(Modal, { open: true, onclose: handleClose });
await page.keyboard.press('Escape');
expect(handleClose).toHaveBeenCalled();
});
it('traps focus within modal', async () => {
render(Modal, { open: true });
const closeButton = page.getByRole('button', { name: 'Close' });
await page.keyboard.press('Tab');
await expect.element(closeButton).toBeFocused();
});
});Dropdown Component
describe('Dropdown', () => {
it('toggles open/closed state', async () => {
render(Dropdown, { options: ['A', 'B', 'C'] });
const trigger = page.getByRole('button');
await trigger.click();
const menu = page.getByRole('listbox');
await expect.element(menu).toBeVisible();
});
it('selects option on click', async () => {
render(Dropdown, { options: ['Apple', 'Banana'] });
await page.getByRole('button').click();
await page.getByText('Banana').click();
const display = page.getByRole('button');
await expect.element(display).toHaveTextContent('Banana');
});
it('handles keyboard navigation', async () => {
render(Dropdown, { options: ['A', 'B', 'C'] });
const trigger = page.getByRole('button');
await trigger.focus();
await page.keyboard.press('Enter');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect.element(trigger).toHaveTextContent('B');
});
});Svelte 5 Runes Testing
Testing $state and $derived
Must use .test.svelte.ts extension:
import { flushSync, untrack } from 'svelte';
describe('Counter with runes', () => {
it('updates reactive state', () => {
let count = $state(0);
let doubled = $derived(count * 2);
expect(count).toBe(0);
expect(untrack(() => doubled)).toBe(0);
count = 5;
flushSync();
expect(count).toBe(5);
expect(untrack(() => doubled)).toBe(10);
});
});Form Validation with Runes
it('validates form with $derived', () => {
let email = $state('');
let isValid = $derived(email.includes('@'));
// Forms start valid (pre-validation)
expect(untrack(() => isValid)).toBe(false);
email = 'user@example.com';
flushSync();
expect(untrack(() => isValid)).toBe(true);
});Integration Testing
Form Submission Flow
describe('Login Form', () => {
it('submits with valid credentials', async () => {
const handleSubmit = vi.fn();
render(LoginForm, { onsubmit: handleSubmit });
// Fill form
await page.getByLabelText('Email').fill('user@example.com');
await page.getByLabelText('Password').fill('password123');
// Submit
await page.getByRole('button', { name: 'Login' }).click();
// Verify
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
// Check success state
const success = page.getByText('Login successful');
await expect.element(success).toBeVisible();
});
});Todo List Operations
describe('Todo List', () => {
it('manages todo lifecycle', async () => {
render(TodoList);
// Add todo
const input = page.getByRole('textbox');
await input.fill('Buy milk');
await page.getByRole('button', { name: 'Add' }).click();
const todo = page.getByText('Buy milk');
await expect.element(todo).toBeInTheDocument();
// Complete todo
const checkbox = page.getByRole('checkbox');
await checkbox.click();
await expect.element(checkbox).toBeChecked();
// Delete todo
await page.getByRole('button', { name: 'Delete' }).click();
await expect.element(todo).not.toBeInTheDocument();
});
});SSR Testing Decision Framework
Always Add SSR Tests For:
- Form components (progressive enhancement)
- Navigation elements (SEO and accessibility)
- Content-heavy components (search engine visibility)
- Layout shells (hydration mismatch prevention)
Skip SSR Tests For:
- Pure interaction components (modals, dropdowns)
- Client-only features (charts, maps)
- Simple presentational elements
SSR Test Example
// my-form.ssr.test.ts
import { render } from 'vitest-browser-svelte/ssr';
import { describe, expect, it } from 'vitest';
import MyForm from './my-form.svelte';
describe('MyForm SSR', () => {
it('renders form fields server-side', async () => {
const { container } = render(MyForm);
const html = container.innerHTML;
expect(html).toContain('input');
expect(html).toContain('type="email"');
});
});Async Patterns
Testing Loading States
describe('Async Data Component', () => {
it('shows loading state', async () => {
render(AsyncComponent);
const loading = page.getByText('Loading...');
await expect.element(loading).toBeVisible();
});
it('displays data after load', async () => {
render(AsyncComponent);
const data = page.getByText('Data loaded');
await expect.element(data).toBeVisible();
});
it('handles errors gracefully', async () => {
render(AsyncComponent, { shouldFail: true });
const error = page.getByText('Error loading data');
await expect.element(error).toBeVisible();
});
});Anti-Patterns to Avoid
❌ Never use containers
// Bad
const { container } = render(Component);
const button = container.querySelector('button');✅ Always use locators
// Good
render(Component);
const button = page.getByRole('button');❌ Don't test implementation details
// Bad
expect(svg).toHaveAttribute('d', 'M10,20...');✅ Test user-visible behavior
// Good
await expect.element(icon).toHaveClass('icon-check');❌ Don't assume element roles
// Bad - input is actually 'textbox'
page.getByRole('input');✅ Verify roles in browser dev tools
// Good
page.getByRole('textbox');Setup & Configuration
Installation
Create New SvelteKit Project
pnpm dlx sv@latest create my-testing-appSelect options:
- Template: SvelteKit minimal
- TypeScript: Yes, using TypeScript syntax
- Additions: Include vitest for unit testing
Install Browser Testing Dependencies
cd my-testing-app
pnpm install -D @vitest/browser-playwright vitest-browser-svelte playwright
pnpm un @testing-library/jest-dom @testing-library/svelte jsdomVite Configuration
Multi-Project Setup
Update vite.config.ts with the "Client-Server Alignment Strategy":
import tailwindcss from '@tailwindcss/vite';
import { playwright } from '@vitest/browser-playwright';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
test: {
projects: [
{
extends: './vite.config.ts',
test: {
name: 'client',
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
setupFiles: ['./vitest-setup-client.ts'],
},
},
{
extends: './vite.config.ts',
test: {
name: 'ssr',
environment: 'node',
include: ['src/**/*.ssr.{test,spec}.{js,ts}'],
},
},
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
},
},
],
},
});Vitest Setup File
Create vitest-setup-client.ts:
/// <reference types="vitest/browser" />
/// <reference types="@vitest/browser-playwright" />File Naming Conventions
*.svelte.test.ts- Client-side component tests (runs in browser)*.ssr.test.ts- Server-side rendering tests (runs in Node)*.test.ts- Server/utility tests (runs in Node)
Running Tests
# Run all tests once
pnpm run test:unit
# Run specific component in watch mode
pnpm vitest src/lib/components/my-button.svelte
# Run only client tests
pnpm vitest --project client
# Run with UI
pnpm vitest --uiPackage.json Scripts
{
"scripts": {
"test:unit": "vitest",
"test:unit:ui": "vitest --ui",
"test:unit:watch": "vitest --watch"
}
}Complete Vitest Browser Mode Setup
Initial Project Setup
If starting a new SvelteKit project:
pnpm dlx sv@latest create my-appSelect during setup:
- Template: "SvelteKit minimal"
- TypeScript: "Yes, using TypeScript syntax"
- Add: prettier, eslint, vitest, playwright
Install Dependencies
pnpm install -D @vitest/browser-playwright vitest-browser-svelte playwright
# Remove old testing library if migrating
pnpm un @testing-library/jest-dom @testing-library/svelte jsdomConfigure Vitest (vite.config.ts)
Multi-project setup for client, SSR, and server tests:
import { sveltekit } from '@sveltejs/kit/vite';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
test: {
projects: [
{
extends: true,
test: {
name: 'client',
testTimeout: 2000,
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: [
'src/lib/server/**',
'src/**/*.ssr.{test,spec}.{js,ts}',
],
setupFiles: ['./src/vitest-setup-client.ts'],
},
},
{
extends: true,
test: {
name: 'ssr',
environment: 'node',
include: ['src/**/*.ssr.{test,spec}.{js,ts}'],
},
},
{
extends: true,
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: [
'src/**/*.svelte.{test,spec}.{js,ts}',
'src/**/*.ssr.{test,spec}.{js,ts}',
],
},
},
],
coverage: {
include: ['src'],
},
},
});Setup File (src/vitest-setup-client.ts)
/// <reference types="vitest/browser" />
/// <reference types="@vitest/browser-playwright" />Run Tests
# Run all tests
pnpm run test:unit
# Watch mode for specific file
pnpm vitest src/lib/components/button.svelte
# Run specific project
pnpm vitest --project=client
pnpm vitest --project=ssr
pnpm vitest --project=serverFile Naming Conventions
*.svelte.test.ts- Component tests (browser mode)*.ssr.test.ts- SSR tests (Node environment)*.test.ts- Server/utility tests (Node environment)
Key Dependencies
vitest- Test framework@vitest/browser-playwright- Browser providervitest-browser-svelte- Svelte-specific renderingplaywright- Browser automation
Troubleshooting
Playwright Installation
If browser binaries are missing:
pnpm exec playwright install chromiumTypeScript Types
Ensure setup file has proper reference directives for IDE support.
Timeout Issues
Increase testTimeout in config if tests timeout (default: 2000ms).
Testing Patterns
Basic Component Test
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import MyButton from './my-button.svelte';
describe('MyButton', () => {
it('should render with correct text', async () => {
render(MyButton, { text: 'Click me' });
const button = page.getByRole('button', { name: 'Click me' });
await expect.element(button).toBeInTheDocument();
});
});Testing with Children (Snippets)
For Svelte 5 components that accept children, use createRawSnippet:
import { createRawSnippet } from 'svelte';
it('should render with children', async () => {
const children = createRawSnippet(() => ({
render: () => `<span>Button Text</span>`,
}));
render(MyButton, { children });
const button = page.getByRole('button', { name: 'Button Text' });
await expect.element(button).toBeInTheDocument();
});Testing Click Events
import { vi } from 'vitest';
it('should handle click events', async () => {
const click_handler = vi.fn();
render(MyButton, { onclick: click_handler, text: 'Click me' });
const button = page.getByRole('button', { name: 'Click me' });
await button.click();
expect(click_handler).toHaveBeenCalledOnce();
});Testing Form Inputs
it('should handle form input', async () => {
render(MyForm);
const input = page.getByLabel('Email address');
await input.fill('[email protected]');
const value = await input.evaluate(
(el) => (el as HTMLInputElement).value,
);
expect(value).toBe('[email protected]');
});Testing Conditional Rendering
it('should show error message on invalid input', async () => {
render(MyForm);
const input = page.getByLabel('Email');
await input.fill('invalid-email');
const submitButton = page.getByRole('button', { name: 'Submit' });
await submitButton.click();
await expect
.element(page.getByText('Invalid email address'))
.toBeInTheDocument();
});Testing Props Changes
it('should update when props change', async () => {
const { rerender } = render(MyComponent, { count: 0 });
await expect
.element(page.getByText('Count: 0'))
.toBeInTheDocument();
rerender({ count: 5 });
await expect
.element(page.getByText('Count: 5'))
.toBeInTheDocument();
});Testing Async Operations
it('should handle async data loading', async () => {
render(MyComponent);
// Initially shows loading state
await expect
.element(page.getByText('Loading...'))
.toBeInTheDocument();
// Wait for data to load
await expect
.element(page.getByText('Data loaded'))
.toBeInTheDocument();
});Testing Multiple Elements
When multiple elements match, use selectors:
it('should handle multiple buttons', async () => {
render(MyComponent);
const buttons = page.getByRole('button', { name: 'Action' });
// Select specific instance
await buttons.first().click();
await buttons.nth(1).click();
await buttons.last().click();
});Testing Disabled States
it('should disable button when loading', async () => {
render(MyButton, { disabled: true });
const button = page.getByRole('button');
await expect.element(button).toBeDisabled();
});Testing CSS Classes
it('should apply correct variant class', async () => {
render(MyButton, { variant: 'primary' });
const button = page.getByRole('button');
const className = await button.evaluate((el) => el.className);
expect(className).toContain('btn-primary');
});Testing Event Modifiers
it('should prevent default on form submit', async () => {
const submit_handler = vi.fn();
render(MyForm, { onsubmit: submit_handler });
const form = page.getByRole('form');
await form.evaluate((el) => {
el.dispatchEvent(new Event('submit', { cancelable: true }));
});
expect(submit_handler).toHaveBeenCalled();
});Testing Accessibility
it('should have accessible name', async () => {
render(MyButton, { 'aria-label': 'Close dialog' });
const button = page.getByRole('button', { name: 'Close dialog' });
await expect.element(button).toBeInTheDocument();
});Mocking Modules
import { vi } from 'vitest';
vi.mock('$app/navigation', () => ({
goto: vi.fn(),
}));
it('should navigate on click', async () => {
const { goto } = await import('$app/navigation');
render(MyLink);
const link = page.getByRole('link', { name: 'Home' });
await link.click();
expect(goto).toHaveBeenCalledWith('/home');
});Troubleshooting
Common Issues and Solutions
Strict Mode Violation Error
Error:
Error: locator.click: strict mode violation: getByRole('button') resolved to 2 elementsCause: Multiple elements match your locator in strict mode.
Solutions:
1. Make the locator more specific:
// Instead of:
page.getByRole('button');
// Use:
page.getByRole('button', { name: 'Submit' });2. Use selectors for expected multiple matches:
page.getByRole('button').first();
page.getByRole('button').nth(1);
page.getByRole('button').last();3. Filter by container:
page.getByTestId('modal').getByRole('button', { name: 'Close' });Test Hangs or Times Out
Symptoms: Test runs indefinitely or hits timeout.
Common Causes:
1. SvelteKit form actions - Don't test actual form submissions:
// Don't do this:
await page.getByRole('button', { name: 'Submit' }).click();
// Do this instead - test state changes:
const { component } = render(MyForm);
// Test component state directly2. Missing await - Always await async operations:
// Wrong:
button.click();
// Correct:
await button.click();3. Waiting for elements that never appear:
// Add timeout or check existence first:
const element = page.getByText('Success');
await expect.element(element).toBeInTheDocument({ timeout: 5000 });Mock Function Signature Mismatch
Error:
Type '() => void' is not assignable to type '(event: CustomEvent) => void'Solution: Match the expected function signature:
// Wrong:
const handler = vi.fn();
// Correct:
const handler = vi.fn((event: CustomEvent) => {});
// Or use type assertion:
const handler = vi.fn() as (event: CustomEvent) => void;Element Not Found
Error:
Error: locator.click: getByRole('button') resolved to 0 elementsDebugging Steps:
1. Check if element is rendered:
// Log the page content:
const content = await page.locator('body').innerHTML();
console.log(content);2. Try different locator strategies:
// Try by text:
page.getByText('Submit');
// Try by test ID:
page.getByTestId('submit-button');
// Try by CSS:
page.locator('button[type="submit"]');3. Wait for element to appear:
await expect
.element(page.getByRole('button'))
.toBeInTheDocument({ timeout: 5000 });Cannot Access .current Property
Error:
Cannot read property 'current' of undefinedCause: Trying to access remote function state incorrectly.
Solution:
// Wrong:
expect(data.current).toBe('value');
// Correct - check if state exists first:
if (data.current) {
expect(data.current.value).toBe('expected');
}Snippets Not Rendering
Error: Children don't render or show empty content.
Solution: Use createRawSnippet correctly:
import { createRawSnippet } from 'svelte';
const children = createRawSnippet(() => ({
render: () => `<span>Content</span>`,
}));
render(MyComponent, { children });Module Mock Not Working
Issue: Mocked module still uses real implementation.
Solution: Ensure mock is defined before import:
import { vi } from 'vitest';
// Mock BEFORE importing the component:
vi.mock('$app/navigation', () => ({
goto: vi.fn(),
}));
// Now import:
import MyComponent from './my-component.svelte';Playwright Browser Not Starting
Error:
browserType.launch: Executable doesn't existSolution: Install Playwright browsers:
pnpm exec playwright install chromium
# Or for all browsers:
pnpm exec playwright installTest File Not Detected
Issue: Vitest doesn't run your test file.
Check:
1. File naming matches config:
- Client:
*.svelte.test.ts - SSR:
*.ssr.test.ts - Server:
*.test.ts
2. File location matches include pattern in vite.config.ts:
include: ['src/**/*.svelte.{test,spec}.{js,ts}'];Type Errors with Vitest Browser
Error:
Cannot find name 'page' or module '@vitest/browser/context'Solution: Add type references to vitest-setup-client.ts:
/// <reference types="@vitest/browser/matchers" />
/// <reference types="@vitest/browser/providers/playwright" />Assertion Not Awaited
Error:
Promise returned from expect was not awaitedSolution: Always await browser assertions:
// Wrong:
expect.element(button).toBeInTheDocument();
// Correct:
await expect.element(button).toBeInTheDocument();Form Input Not Updating
Issue: Input value doesn't change when using .fill().
Solution:
1. Ensure element is an input:
const input = page.getByLabel('Email');
await expect.element(input).toBeInTheDocument();
await input.fill('[email protected]');2. Try dispatching input event:
await input.evaluate((el, value) => {
(el as HTMLInputElement).value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
}, '[email protected]');Component State Not Updating
Issue: Props change but UI doesn't update.
Solution: Use rerender and wait for updates:
const { rerender } = render(MyComponent, { count: 0 });
await expect.element(page.getByText('0')).toBeInTheDocument();
rerender({ count: 1 });
// Wait for update:
await expect.element(page.getByText('1')).toBeInTheDocument();Debugging Tips
1. Use Vitest UI
pnpm vitest --uiProvides visual test runner with debugging tools.
2. Add Screenshots
import { page } from '@vitest/browser/context';
it('should work', async () => {
render(MyComponent);
await page.screenshot({ path: 'debug.png' });
});3. Pause Test Execution
import { page } from '@vitest/browser/context';
it('should work', async () => {
render(MyComponent);
await page.pause(); // Opens browser inspector
});4. Console Logs
// Log component HTML:
const html = await page.locator('body').innerHTML();
console.log(html);
// Log element properties:
const button = page.getByRole('button');
const text = await button.textContent();
console.log('Button text:', text);5. Increase Timeout
// Per test:
it('slow test', { timeout: 10000 }, async () => {
// test code
});
// Per assertion:
await expect.element(button).toBeInTheDocument({ timeout: 5000 });Performance Issues
Tests Running Slowly
Solutions:
1. Reduce testTimeout in config:
browser: {
testTimeout: 2000, // Default is 5000
}2. Run tests in parallel (default in Vitest)
3. Use .skip() or .only() during development:
it.only('this test only', async () => {});
it.skip('skip this', async () => {});Too Many Browser Instances
Solution: Limit instances in config:
browser: {
instances: [{ browser: 'chromium' }],
headless: true,
}