API Reference
Complete reference for vitest-browser-svelte testing APIs, organized by immediate developer needs. These APIs support the Client-Server Alignment Strategy for reliable full-stack testing.
Quick Start Imports
Essential Setup
1import { describe, expect, test, vi } from 'vitest';
2import { render } from 'vitest-browser-svelte';
3import { page } from 'vitest/browser';Svelte 5 Runes & SSR
1import { createRawSnippet } from 'svelte';
2import { flushSync, untrack } from 'svelte';
3import { render } from 'svelte/server'; // SSR testing onlyServer Testing (Client-Server Alignment)
1// Real web APIs for server tests - no mocking
2const form_data = new FormData();
3const request = new Request('http://localhost/api/endpoint', {
4 method: 'POST',
5 body: form_data,
6});π― Locators (Auto-Retry Built-in)
CRITICAL: Always use locators, never containers. Locators have automatic waiting and retrying.
Semantic Queries (Preferred)
1// β
Buttons - test accessibility
2page.getByRole('button', { name: 'Submit' });
3page.getByRole('button', { name: /submit/i }); // Case insensitive
4
5// β
Form controls - semantic HTML
6page.getByRole('textbox', { name: 'Email' }); // <input type="text">
7page.getByRole('checkbox', { name: 'Remember me' });
8page.getByRole('combobox', { name: 'Country' }); // <select>
9
10// β
Navigation & structure
11page.getByRole('link', { name: 'Documentation' });
12page.getByRole('heading', { level: 1 });
13page.getByRole('main');
14page.getByRole('navigation');Form-Specific Queries
1// β
Labels - best for forms
2page.getByLabel('Email address');
3page.getByLabel('Password');
4page.getByLabel(/phone/i);
5
6// β
Placeholders - when no label
7page.getByPlaceholder('Enter your email');
8page.getByPlaceholder(/search/i);Content Queries
1// β
Text content
2page.getByText('Welcome back');
3page.getByText('Welcome', { exact: false }); // Partial match
4page.getByText(/welcome/i); // RegexTest ID Fallback
1// β
Only when semantic queries aren't possible
2page.getByTestId('submit-button');
3page.getByTestId('error-message');
4page.getByTestId('loading-spinner');π¨ Handle Multiple Elements (Strict Mode)
1// β FAILS: "strict mode violation" - multiple elements
2page.getByRole('link', { name: 'Home' });
3
4// β
CORRECT: Use .first(), .nth(), .last()
5page.getByRole('link', { name: 'Home' }).first();
6page.getByRole('listitem').nth(2); // Zero-indexed
7page.getByRole('button').last();
8
9// β
Filter for specificity
10page.getByRole('button').filter({ hasText: 'Delete' });
11
12// β
Chain for context
13page.getByRole('dialog').getByRole('button', { name: 'Close' });π Assertions (Always Await)
Element Presence
1// β
Always await element assertions
2await expect.element(page.getByText('Success')).toBeInTheDocument();
3await expect.element(page.getByRole('button')).toBeVisible();
4await expect.element(page.getByTestId('error')).toBeHidden();
5await expect.element(page.getByRole('dialog')).toBeAttached();Element States
1// β
Interactive states
2await expect.element(page.getByRole('button')).toBeEnabled();
3await expect.element(page.getByRole('button')).toBeDisabled();
4await expect.element(page.getByRole('checkbox')).toBeChecked();
5await expect.element(page.getByRole('textbox')).toBeFocused();Content & Attributes
1// β
Text content
2await expect.element(page.getByRole('heading')).toHaveText('Welcome');
3await expect.element(page.getByTestId('counter')).toContainText('5');
4
5// β
Form values
6await expect
7 .element(page.getByRole('textbox'))
8 .toHaveValue('[email protected]');
9
10// β
Attributes & classes
11await expect
12 .element(page.getByRole('link'))
13 .toHaveAttribute('href', '/docs');
14await expect
15 .element(page.getByRole('button'))
16 .toHaveClass('btn-primary');Count Assertions
1// β
Exact count
2await expect.element(page.getByRole('listitem')).toHaveCount(3);
3
4// β
Range counts
5await expect
6 .element(page.getByRole('button'))
7 .toHaveCount({ min: 1 });
8await expect
9 .element(page.getByRole('button'))
10 .toHaveCount({ max: 5 });π±οΈ User Interactions
Click Events
1// β
Simple click
2await page.getByRole('button', { name: 'Submit' }).click();
3
4// β
Force click (bypass animations)
5await page.getByRole('button').click({ force: true });
6
7// β
Advanced click options
8await page.getByRole('button').click({
9 button: 'right', // Right click
10 clickCount: 2, // Double click
11 position: { x: 10, y: 20 }, // Specific position
12});Form Interactions
1// β
Fill inputs
2await page
3 .getByRole('textbox', { name: 'Email' })
4 .fill('[email protected]');
5
6// β
Clear and refill
7await page.getByRole('textbox').clear();
8await page.getByRole('textbox').fill('new-value');
9
10// β
Checkboxes and selects
11await page.getByRole('checkbox').check();
12await page.getByRole('checkbox').uncheck();
13await page.getByRole('combobox').selectOption('value');
14await page.getByRole('combobox').selectOption(['value1', 'value2']);
15
16// β
File uploads
17await page
18 .getByRole('textbox', { name: 'Upload' })
19 .setInputFiles('path/to/file.txt');Keyboard Interactions
1// β
Key presses
2await page.keyboard.press('Enter');
3await page.keyboard.press('Escape');
4await page.keyboard.press('Tab');
5
6// β
Key combinations
7await page.keyboard.press('Control+A');
8await page.keyboard.press('Shift+Tab');
9
10// β
Type text
11await page.keyboard.type('Hello World');
12
13// β
Element-specific keyboard
14await page.getByRole('textbox').press('Enter');π Component Rendering
Basic Rendering
1// β
Simple component with snake_case props
2await render(Button, {
3 variant: 'primary',
4 is_disabled: false,
5 click_handler: vi.fn(),
6});
7
8// β
Form component with validation
9await render(Input, {
10 input_type: 'email',
11 label_text: 'Email',
12 current_value: '[email protected]',
13 error_message: 'Invalid email',
14 is_required: true,
15});Advanced Rendering
1// β
Event handlers with snake_case
2const handle_click = vi.fn();
3const handle_submit = vi.fn();
4
5await render(Button, {
6 onclick: handle_click,
7 onsubmit: handle_submit,
8 children: 'Click me',
9});
10
11// β
Svelte 5 snippets (limited support)
12const children = createRawSnippet(() => ({
13 render: () => `<span>Custom content</span>`, // Must return HTML
14}));
15await render(Modal, { children });
16
17// β
Component with context
18await render(
19 Component,
20 { user_data: { name: 'Test' } },
21 { context: new Map([['theme', 'dark']]) },
22);π Svelte 5 Runes Testing
State Testing
1// β
$state - direct testing
2test('reactive state updates', () => {
3 let count = $state(0);
4 expect(count).toBe(0);
5
6 count = 5;
7 expect(count).toBe(5);
8});
9
10// β
$derived - ALWAYS use untrack()
11test('derived state calculation', () => {
12 let count = $state(0);
13 let doubled = $derived(count * 2);
14
15 // CRITICAL: Always untrack derived values
16 expect(untrack(() => doubled)).toBe(0);
17
18 count = 5;
19 flushSync(); // Force synchronous update
20 expect(untrack(() => doubled)).toBe(10);
21});
22
23// β
Complex derived with getters
24test('derived getter functions', () => {
25 const form_state = create_form_state();
26 const is_valid_getter = form_state.is_form_valid;
27
28 // Get function first, then untrack
29 expect(untrack(() => is_valid_getter())).toBe(true);
30});Effect Testing
1// β
$effect with spy functions
2test('effect runs on state change', () => {
3 const effect_spy = vi.fn();
4 let count = $state(0);
5
6 $effect(() => {
7 effect_spy(count);
8 });
9
10 count = 1;
11 flushSync();
12
13 expect(effect_spy).toHaveBeenCalledWith(1);
14});π₯οΈ SSR Testing
Component Rendering
1import { render } from 'svelte/server';
2
3// β
Basic SSR render
4const { body, head } = render(Component);
5
6// β
With props using snake_case
7const { body, head } = render(Component, {
8 props: {
9 page_title: 'Test Page',
10 user_data: { name: 'Test User' },
11 },
12});
13
14// β
With context
15const { body, head } = render(Component, {
16 props: {},
17 context: new Map([['theme', 'dark']]),
18});SSR Assertions
1// β
Content structure (not implementation details)
2expect(body).toContain('<h1>Welcome</h1>');
3expect(body).toContain('role="main"');
4expect(body).toContain('aria-label="Navigation"');
5
6// β
Head content for SEO
7expect(head).toContain('<title>Page Title</title>');
8expect(head).toContain('<meta name="description"');
9
10// β AVOID: Testing exact SVG paths or implementation details
11// expect(body).toContain('M9 12l2 2 4-4m6 2a9'); // Brittle!
12
13// β
BETTER: Test semantic structure
14expect(body).toContain('<svg');
15expect(body).toContain('text-success');π Mocking (Minimal - Real Browser Testing)
PRINCIPLE: In vitest-browser-svelte, render real components. Mock only when necessary.
Component Mocking Decision Tree
1Is component EXTERNAL? β Mock it
2Is component STATELESS/PRESENTATIONAL? β Mock it
3Does component have COMPLEX LOGIC? β Mock for unit, render for integration
4DEFAULT β Render the componentWhen to Mock Components
1// β
Mock EXTERNAL components (third-party libraries)
2vi.mock('@external/heavy-chart', () => ({
3 default: vi.fn(() => ({
4 $$: {},
5 $set: vi.fn(),
6 $destroy: vi.fn(),
7 })),
8}));
9
10// β
Mock STATELESS presentational components in unit tests
11vi.mock('#lib/components/icon.svelte', () => ({
12 default: vi.fn(() => ({
13 $$: {},
14 $set: vi.fn(),
15 $destroy: vi.fn(),
16 })),
17}));
18
19// β DON'T mock your own components with logic - render them!
20// render(MyComplexComponent); // Test the real thingFunction & Module Mocking
1// β
Mock utility functions with snake_case
2const mock_validate_email = vi.fn(() => true);
3const mock_api_call = vi.fn((user_id: string) => ({
4 user_id,
5 user_name: 'Test User',
6 is_active: true,
7}));
8
9// β
Mock external APIs and services
10vi.mock('#lib/api.js', () => ({
11 fetch_user_data: vi.fn(() => Promise.resolve({ user_id: 1 })),
12 send_analytics: vi.fn(),
13}));
14
15// β
Spy on existing functions when needed
16const validate_spy = vi.spyOn(utils, 'validate_email');β±οΈ Wait Utilities
Element Waiting
1// β
Wait for elements (built into locators)
2await expect
3 .element(page.getByText('Loading complete'))
4 .toBeInTheDocument();
5
6// β
Custom timeout
7await expect
8 .element(page.getByText('Data loaded'))
9 .toBeInTheDocument({ timeout: 10000 });
10
11// β
Wait for disappearance
12await expect
13 .element(page.getByText('Loading...'))
14 .not.toBeInTheDocument();Custom Conditions
1// β
Wait for JavaScript conditions
2await page.waitForFunction(() => window.data_loaded === true);
3
4// β
Wait for network requests
5await page.waitForResponse('**/api/user-data');
6
7// β
Simple timeout (use sparingly)
8await page.waitForTimeout(1000);π¨ Error Handling & Edge Cases
Form Validation Testing
1// β
Test validation lifecycle: valid β validate β invalid β fix β valid
2test('form validation lifecycle', async () => {
3 const form_state = create_form_state({
4 email: { value: '', validation_rules: { required: true } },
5 });
6
7 // Initially valid (no validation run yet)
8 expect(untrack(() => form_state.is_form_valid())).toBe(true);
9
10 // Trigger validation - now invalid
11 form_state.validate_all_fields();
12 expect(untrack(() => form_state.is_form_valid())).toBe(false);
13
14 // Fix the error - valid again
15 form_state.update_field('email', '[email protected]');
16 expect(untrack(() => form_state.is_form_valid())).toBe(true);
17});Component Error Testing
1// β
Test error boundaries
2expect(() => {
3 render(BrokenComponent);
4}).toThrow('Component error');
5
6// β
Test error states
7await render(Component, {
8 props: {
9 error_message: 'Something went wrong',
10 has_error: true,
11 },
12});
13await expect
14 .element(page.getByText('Something went wrong'))
15 .toBeInTheDocument();Assertion Error Handling
1// β
Handle expected assertion failures
2try {
3 await expect
4 .element(page.getByText('Nonexistent'))
5 .toBeInTheDocument();
6} catch (error) {
7 expect(error.message).toContain('Element not found');
8}π οΈ Custom Utilities
Test Helpers
1// β
Custom render helper with snake_case
2const render_with_theme = async (Component: any, props = {}) => {
3 return await render(Component, {
4 ...props,
5 context: new Map([['theme', 'dark']]),
6 });
7};
8
9// β
Form testing helper
10const fill_form_data = async (form_data: Record<string, string>) => {
11 for (const [field_name, field_value] of Object.entries(form_data)) {
12 await page.getByLabelText(field_name).fill(field_value);
13 }
14};
15
16// β
Loading state helper
17const wait_for_loading_complete = async () => {
18 await expect
19 .element(page.getByTestId('loading-spinner'))
20 .not.toBeInTheDocument();
21};Custom Matchers
1// β
Extend expect with domain-specific matchers
2expect.extend({
3 to_have_validation_error(received: any, expected_error: string) {
4 const error_element = page.getByText(expected_error);
5 const element_exists = !!error_element;
6
7 return {
8 pass: element_exists,
9 message: () =>
10 element_exists
11 ? `Expected not to have validation error: ${expected_error}`
12 : `Expected to have validation error: ${expected_error}`,
13 };
14 },
15});βοΈ Configuration Reference
Vitest Browser Config
1// vite.config.ts
2import { sveltekit } from '@sveltejs/kit/vite';
3import { playwright } from '@vitest/browser-playwright';
4import { defineConfig } from 'vitest/config';
5
6export default defineConfig({
7 plugins: [sveltekit()],
8 test: {
9 expect: { requireAssertions: true },
10 projects: [
11 {
12 extends: './vite.config.ts',
13 test: {
14 include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
15 exclude: ['src/lib/server/**'],
16 name: 'client',
17 browser: {
18 enabled: true,
19 provider: playwright(),
20 instances: [{ browser: 'chromium', headless: true }],
21 },
22 },
23 },
24 {
25 extends: './vite.config.ts',
26 test: {
27 include: ['src/**/*.{test,spec}.{js,ts}'],
28 exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
29 name: 'server',
30 environment: 'node',
31 },
32 },
33 ],
34 },
35});This follows the CLIβs two-project baseline. See SSR testing for Sveltestβs optional third
project. Playwright uses testMatch: '**/*.e2e.{ts,js}' independently
of Vitest, so E2E tests can also live beside routes. Importing render registers cleanup without a setup file; await it with
renderer version 3.
For Vite+ projects, import configuration from vite-plus, test APIs
from vite-plus/test, and browser APIs from vite-plus/test/browser.
Keep providers and coverage aligned with its bundled Vitest version.
See the migration guide.
Test Environment Setup
1// β
Environment variables
2process.env.NODE_ENV = 'test';
3process.env.API_URL = 'http://localhost:3000';
4
5// β
Custom timeouts
6test(
7 'slow integration test',
8 async () => {
9 // Test implementation
10 },
11 { timeout: 30000 },
12);
13
14// β
Test-specific configuration
15test.concurrent('parallel test', async () => {
16 // Runs in parallel with other concurrent tests
17});π« Critical Anti-Patterns
β Never Use Containers
1// β NEVER - No auto-retry, manual DOM queries
2const { container } = render(MyComponent);
3const button = container.querySelector('[data-testid="submit"]');
4
5// β
ALWAYS - Auto-retry, semantic queries
6await render(MyComponent);
7const button = page.getByTestId('submit');
8await button.click();β Donβt Test Implementation Details
1// β BRITTLE - Tests exact SVG path data
2expect(body).toContain(
3 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
4);
5
6// β
ROBUST - Tests user-visible behavior
7await expect
8 .element(page.getByRole('img', { name: /success/i }))
9 .toBeInTheDocument();β Donβt Click Form Submits
1// β Can cause hangs with SvelteKit enhance
2await page.getByRole('button', { name: 'Submit' }).click();
3
4// β
Test form state directly
5await render(MyForm, { props: { errors: { email: 'Required' } } });
6await expect.element(page.getByText('Required')).toBeInTheDocument();π Quick Reference
Essential Patterns
- β
Use
page.getBy*()locators - never containers - β
Always
await expect.element()for assertions - β
Use
.first(),.nth(),.last()for multiple elements - β
Use
untrack()for$derivedvalues - β
Use
force: truefor animations - β Use snake_case for variables/functions
- β Test form validation lifecycle
- β Handle strict mode violations properly
Common Fixes
- βstrict mode violationβ: Use
.first(),.nth(),.last() - Role confusion: Links with
role="button"are buttons - Input elements: Use
getByRole('textbox'), notgetByRole('input') - Derived values: Always use
untrack(() => derived_value) - Form validation: Test initial valid β validate β invalid β fix β valid