Testing Patterns

Overview

This guide provides specific, actionable testing patterns for common scenarios in Svelte 5 applications.

For comprehensive best practices and philosophy, see Best Practices. For setup and configuration, see Getting Started. For Svelte 5 runes testing ($state, $derived, untrack(), flushSync()), see Runes Testing.

Component Testing

For locator patterns, handling multiple elements, role confusion fixes, component patterns (Button, Input, Modal, Dropdown), and mocking patterns, see Component Testing.

Integration Testing Patterns

Form Submission Pattern

1describe('Contact Form Integration', () => {
2	it('should handle complete form submission flow', async () => {
3		const submit_handler = vi.fn();
4		await render(ContactForm, { onsubmit: submit_handler });
5
6		// Fill out form
7		const name_input = page.getByLabelText('Name');
8		const email_input = page.getByLabelText('Email');
9		const message_input = page.getByLabelText('Message');
10
11		await name_input.fill('John Doe');
12		await email_input.fill('[email protected]');
13		await message_input.fill('Hello world');
14
15		// Submit form
16		const submit_button = page.getByRole('button', {
17			name: 'Send Message',
18		});
19		await submit_button.click();
20
21		// Verify submission
22		expect(submit_handler).toHaveBeenCalledWith({
23			name: 'John Doe',
24			email: '[email protected]',
25			message: 'Hello world',
26		});
27
28		// Verify success message
29		await expect
30			.element(page.getByText('Message sent successfully'))
31			.toBeInTheDocument();
32	});
33
34	it('should prevent submission with invalid data', async () => {
35		const submit_handler = vi.fn();
36		await render(ContactForm, { onsubmit: submit_handler });
37
38		// Try to submit empty form
39		const submit_button = page.getByRole('button', {
40			name: 'Send Message',
41		});
42		await submit_button.click();
43
44		// Should show validation errors
45		await expect
46			.element(page.getByText('Name is required'))
47			.toBeInTheDocument();
48		await expect
49			.element(page.getByText('Email is required'))
50			.toBeInTheDocument();
51
52		// Should not call submit handler
53		expect(submit_handler).not.toHaveBeenCalled();
54	});
55});

Todo List Pattern

1describe('Todo List Integration', () => {
2	it('should handle complete todo lifecycle', async () => {
3		await render(TodoManager);
4
5		// Add todo
6		const input = page.getByLabelText('New todo');
7		await input.fill('Buy groceries');
8
9		const add_button = page.getByRole('button', { name: 'Add Todo' });
10		await add_button.click();
11
12		// Verify todo appears
13		const todo_item = page.getByText('Buy groceries');
14		await expect.element(todo_item).toBeInTheDocument();
15
16		// Complete todo
17		const checkbox = page.getByRole('checkbox', {
18			name: 'Mark Buy groceries as complete',
19		});
20		await checkbox.check();
21
22		// Verify completion styling
23		await expect.element(checkbox).toBeChecked();
24		await expect.element(todo_item).toHaveClass('todo-completed');
25
26		// Delete todo
27		const delete_button = page.getByRole('button', {
28			name: 'Delete Buy groceries',
29		});
30		await delete_button.click();
31
32		// Verify removal
33		await expect.element(todo_item).not.toBeInTheDocument();
34	});
35});
1describe('Navigation Integration', () => {
2	it('should navigate between pages', async () => {
3		await render(AppLayout);
4
5		// Navigate to docs
6		const docs_link = page
7			.getByRole('link', { name: 'Documentation' })
8			.first();
9		await docs_link.click();
10
11		await expect
12			.element(page.getByText('Getting Started'))
13			.toBeInTheDocument();
14
15		// Navigate to examples
16		const examples_link = page
17			.getByRole('link', { name: 'Examples' })
18			.first();
19		await examples_link.click();
20
21		await expect
22			.element(page.getByText('Example Components'))
23			.toBeInTheDocument();
24	});
25
26	it('should highlight active navigation', async () => {
27		await render(AppLayout, { current_page: '/docs' });
28
29		const docs_link = page
30			.getByRole('link', { name: 'Documentation' })
31			.first();
32		await expect.element(docs_link).toHaveClass('nav-active');
33
34		const home_link = page
35			.getByRole('link', { name: 'Home' })
36			.first();
37		await expect.element(home_link).not.toHaveClass('nav-active');
38	});
39});

SSR Testing Patterns

For comprehensive SSR testing guidance including when to add SSR tests, basic patterns, layout patterns, and head/body testing, see SSR Testing.

Server Testing Patterns

For comprehensive server testing patterns including API routes, form actions, and server hooks, see Server Testing.

Error Handling Patterns

Async Error Pattern

1describe('Async Component', () => {
2	it('should handle loading states', async () => {
3		await render(AsyncDataComponent);
4
5		// Should show loading initially
6		await expect
7			.element(page.getByText('Loading...'))
8			.toBeInTheDocument();
9
10		// Wait for data to load
11		await expect
12			.element(page.getByText('Data loaded'))
13			.toBeInTheDocument();
14		await expect
15			.element(page.getByText('Loading...'))
16			.not.toBeInTheDocument();
17	});
18
19	it('should handle error states', async () => {
20		// Mock API to throw error
21		vi.mocked(fetch_data).mockRejectedValueOnce(
22			new Error('API Error'),
23		);
24
25		await render(AsyncDataComponent);
26
27		await expect
28			.element(page.getByText('Error: API Error'))
29			.toBeInTheDocument();
30	});
31});

Form Error Pattern

1describe('Form Error Handling', () => {
2	it('should display server errors', async () => {
3		const submit_handler = vi.fn().mockRejectedValueOnce({
4			message: 'Server error',
5			field_errors: { email: 'Email already exists' },
6		});
7
8		await render(RegistrationForm, { onsubmit: submit_handler });
9
10		const submit_button = page.getByRole('button', {
11			name: 'Register',
12		});
13		await submit_button.click();
14
15		await expect
16			.element(page.getByText('Server error'))
17			.toBeInTheDocument();
18		await expect
19			.element(page.getByText('Email already exists'))
20			.toBeInTheDocument();
21	});
22});

Performance Testing Patterns

Large List Pattern

1describe('Large List Performance', () => {
2	it('should handle large datasets', async () => {
3		const large_dataset = Array.from({ length: 1000 }, (_, i) => ({
4			id: i,
5			name: `Item ${i}`,
6		}));
7
8		await render(VirtualizedList, { items: large_dataset });
9
10		// Should render without hanging
11		await expect
12			.element(page.getByText('Item 0'))
13			.toBeInTheDocument();
14
15		// Should support scrolling
16		const list_container = page.getByTestId('list-container');
17		await list_container.scroll({ top: 5000 });
18
19		// Should render items further down
20		await expect
21			.element(page.getByText('Item 50'))
22			.toBeInTheDocument();
23	});
24});

Debounced Input Pattern

1describe('Search Input Performance', () => {
2	it('should debounce search queries', async () => {
3		const search_handler = vi.fn();
4		await render(SearchInput, { onsearch: search_handler });
5
6		const input = page.getByLabelText('Search');
7
8		// Type quickly
9		await input.fill('a');
10		await input.fill('ab');
11		await input.fill('abc');
12
13		// Should not call handler immediately
14		expect(search_handler).not.toHaveBeenCalled();
15
16		// Wait for debounce
17		await new Promise((resolve) => setTimeout(resolve, 500));
18
19		// Should call handler once with final value
20		expect(search_handler).toHaveBeenCalledOnce();
21		expect(search_handler).toHaveBeenCalledWith('abc');
22	});
23});

Quick Reference

Essential Patterns Checklist

  • ✅ Use page.getBy*() locators - never containers
  • ✅ Always await locator assertions: await expect.element()
  • ✅ Use .first(), .nth(), .last() for multiple elements
  • ✅ Use untrack() for $derived: expect(untrack(() => derived_value))
  • ✅ Use force: true for animations: await element.click({ force: true })
  • ✅ Test form validation lifecycle: initial (valid) → validate → fix
  • ✅ Use snake_case for variables/functions, kebab-case for files
  • ✅ Handle role confusion: textbox not input, check actual role attributes

Common Fixes

  • “strict mode violation”: Use .first(), .nth(), .last()
  • Role confusion: Links with role="button" are buttons, use getByRole('button')
  • Input elements: Use getByRole('textbox'), not getByRole('input')
  • Form hangs: Don’t click SvelteKit form submits - test state directly
  • Animation issues: Use force: true for click events

Anti-Patterns to Avoid

  • ❌ Never use containers: const { container } = render()
  • ❌ Don’t ignore strict mode violations
  • ❌ Don’t assume element roles - verify with browser dev tools
  • ❌ Don’t expect forms to be invalid initially
  • ❌ Don’t click SvelteKit form submits in tests