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 only

Server 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); // Regex

Test 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 component

When 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 thing

Function & 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 $derived values
  • βœ… Use force: true for 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'), not getByRole('input')
  • Derived values: Always use untrack(() => derived_value)
  • Form validation: Test initial valid β†’ validate β†’ invalid β†’ fix β†’ valid