Best Practices

Foundation First Approach

The Strategic Test Planning Method

Start with complete test structure using describe and it.skip to plan comprehensively:

import { describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { page } from '@vitest/browser/context';

describe('TodoManager Component', () => {
	describe('Initial Rendering', () => {
		it('should render empty state', async () => {
			render(TodoManager);

			await expect
				.element(page.getByText('No todos yet'))
				.toBeInTheDocument();
			await expect
				.element(page.getByRole('list'))
				.toHaveAttribute('aria-label', 'Todo list');
		});

		it.skip('should render with initial todos', async () => {
			// TODO: Test with pre-populated data
		});
	});

	describe('User Interactions', () => {
		it('should add new todo', async () => {
			render(TodoManager);

			const input = page.getByLabelText('New todo');
			const add_button = page.getByRole('button', {
				name: 'Add Todo',
			});

			await input.fill('Buy groceries');
			await add_button.click();

			await expect
				.element(page.getByText('Buy groceries'))
				.toBeInTheDocument();
		});

		it.skip('should edit existing todo', async () => {
			// TODO: Test inline editing
		});

		it.skip('should delete todo', async () => {
			// TODO: Test deletion flow
		});
	});

	describe('Form Validation', () => {
		it.skip('should prevent empty todo submission', async () => {
			// TODO: Test validation rules
		});

		it.skip('should handle duplicate todos', async () => {
			// TODO: Test duplicate prevention
		});
	});

	describe('Accessibility', () => {
		it.skip('should support keyboard navigation', async () => {
			// TODO: Test tab order and shortcuts
		});

		it.skip('should announce changes to screen readers', async () => {
			// TODO: Test ARIA live regions
		});
	});

	describe('Edge Cases', () => {
		it.skip('should handle network failures gracefully', async () => {
			// TODO: Test offline scenarios
		});

		it.skip('should handle large todo lists', async () => {
			// TODO: Test performance with 1000+ items
		});
	});
});

Benefits of Foundation First

  • Complete picture: See all requirements upfront
  • Incremental progress: Remove .skip as you implement features
  • No forgotten tests: All edge cases planned from start
  • Team alignment: Everyone sees the testing scope
  • Flexible coverage: Implement tests as needed, not for arbitrary coverage metrics

Client-Server Alignment Strategy

The Problem with Heavy Mocking

The Issue: Server unit tests with heavy mocking can pass while production breaks due to client-server mismatches. Forms send data in one format, servers expect another, and mocked tests miss the disconnect.

Real-World Example: Your client sends FormData with field names like email, but your server expects user_email. Mocked tests pass because they don’t use real FormData objects, but production fails silently.

The Multi-Layer Testing Solution

This project demonstrates a strategic approach with minimal mocking:

// ❌ BRITTLE: Heavy mocking hides client-server mismatches
describe('User Registration - WRONG WAY', () => {
	it('should register user', async () => {
		const mock_request = {
			formData: vi.fn().mockResolvedValue({
				get: vi.fn().mockReturnValue('[email protected]'),
			}),
		};

		// This passes but doesn't test real FormData behavior
		const result = await register_user(mock_request);
		expect(result.success).toBe(true);
	});
});

// ✅ ROBUST: Real FormData objects catch actual mismatches
describe('User Registration - CORRECT WAY', () => {
	it('should register user with real FormData', async () => {
		const form_data = new FormData();
		form_data.append('email', '[email protected]');
		form_data.append('password', 'secure123');

		const request = new Request('http://localhost/register', {
			method: 'POST',
			body: form_data,
		});

		// Only mock external services (database), not data structures
		vi.mocked(database.create_user).mockResolvedValue({
			id: '123',
			email: '[email protected]',
		});

		const result = await register_user(request);
		expect(result.success).toBe(true);
	});
});

Four-Layer Testing Strategy

1. Shared Validation Logic

// lib/validation/user-schema.ts
export const user_registration_schema = {
	email: { required: true, type: 'email' },
	password: { required: true, min_length: 8 },
};

// Used in both client and server
export const validate_user_registration = (data: FormData) => {
	const email = data.get('email')?.toString();
	const password = data.get('password')?.toString();

	// Same validation logic everywhere
	return {
		email: validate_email(email),
		password: validate_password(password),
	};
};

2. Real FormData/Request Objects in Server Tests

describe('Registration API', () => {
	it('should handle real form submission', async () => {
		// Real FormData - catches field name mismatches
		const form_data = new FormData();
		form_data.append('email', '[email protected]');
		form_data.append('password', 'secure123');

		// Real Request object - catches header/method issues
		const request = new Request('http://localhost/api/register', {
			method: 'POST',
			body: form_data,
			headers: { 'Content-Type': 'multipart/form-data' },
		});

		// Only mock external services
		vi.mocked(database.users.create).mockResolvedValue({
			id: '123',
			email: '[email protected]',
		});

		const response = await POST({ request });
		expect(response.status).toBe(201);
	});
});

3. TypeScript Contracts

// lib/types/user.ts
export interface UserRegistration {
	email: string;
	password: string;
}

export interface UserResponse {
	id: string;
	email: string;
	created_at: string;
}

// Both client and server use the same types
// Compiler catches mismatches at build time

4. E2E Safety Net

// e2e/registration.spec.ts
test('full registration flow', async ({ page }) => {
	await page.goto('/register');

	await page.getByLabelText('Email').fill('[email protected]');
	await page.getByLabelText('Password').fill('secure123');
	await page.getByRole('button', { name: 'Register' }).click();

	// Tests the complete client-server integration
	await expect(page.getByText('Welcome!')).toBeVisible();
});

Benefits of This Approach

  • Fast unit test feedback with minimal mocking overhead
  • Confidence that client and server actually work together
  • Catches contract mismatches early in development
  • Reduces production bugs from client-server disconnects
  • Maintains test speed while improving reliability

What to Mock vs What to Keep Real

✅ Mock These (External Dependencies)

// Database operations
vi.mock('$lib/database', () => ({
	users: {
		create: vi.fn(),
		find_by_email: vi.fn(),
	},
}));

// External APIs
vi.mock('$lib/email-service', () => ({
	send_welcome_email: vi.fn(),
}));

// File system operations
vi.mock('fs/promises', () => ({
	writeFile: vi.fn(),
	readFile: vi.fn(),
}));

❌ Keep These Real (Data Contracts)

// ✅ Real FormData objects
const form_data = new FormData();
form_data.append('email', '[email protected]');

// ✅ Real Request/Response objects
const request = new Request('http://localhost/api/users', {
	method: 'POST',
	body: form_data,
});

// ✅ Real validation functions
const validation_result = validate_user_input(form_data);

// ✅ Real data transformation utilities
const formatted_data = format_user_data(raw_input);

Always Use Locators, Never Containers

The Critical vitest-browser-svelte Pattern

NEVER use containers - they don’t have auto-retry and require manual DOM queries:

// ❌ NEVER use containers - no auto-retry, manual DOM queries
it('should handle button click - WRONG WAY', async () => {
	const { container } = render(MyComponent);
	const button = container.querySelector('[data-testid="submit"]');
	// This can fail randomly due to timing issues
});

// ✅ ALWAYS use locators - auto-retry, semantic queries
it('should handle button click', async () => {
	render(MyComponent);
	const button = page.getByTestId('submit');
	await button.click(); // Automatic waiting and retrying

	await expect.element(page.getByText('Success')).toBeInTheDocument();
});

Locator Patterns with Auto-retry

describe('Locator Best Practices', () => {
	it('should use semantic queries first', async () => {
		render(LoginForm);

		// ✅ Semantic queries (preferred - test accessibility)
		const email_input = page.getByRole('textbox', { name: 'Email' });
		const password_input = page.getByLabelText('Password');
		const submit_button = page.getByRole('button', {
			name: 'Sign In',
		});

		await email_input.fill('[email protected]');
		await password_input.fill('password123');
		await submit_button.click();

		await expect
			.element(page.getByText('Welcome back!'))
			.toBeInTheDocument();
	});

	it('should handle multiple elements with strict mode', async () => {
		render(NavigationMenu);

		// ❌ FAILS: Multiple elements match
		// page.getByRole('link', { name: 'Home' });

		// ✅ CORRECT: Use .first(), .nth(), .last()
		const home_link = page
			.getByRole('link', { name: 'Home' })
			.first();
		await home_link.click();

		await expect
			.element(page.getByHeading('Welcome Home'))
			.toBeInTheDocument();
	});

	it('should use test ids when semantic queries are not possible', async () => {
		render(ComplexWidget);

		// ✅ Test IDs (when semantic queries aren't possible)
		const widget = page.getByTestId('complex-widget');
		await expect.element(widget).toBeInTheDocument();

		// Still prefer semantic queries for interactions
		const action_button = page.getByRole('button', {
			name: 'Process Data',
		});
		await action_button.click();
	});
});

Avoid Testing Implementation Details

Focus on User Value, Not Internal Structure

NEVER test exact implementation details that provide no user value:

// ❌ BRITTLE ANTI-PATTERN - Tests exact SVG path data
it('should render check icon - WRONG WAY', () => {
	const { body } = render(StatusIcon, { status: 'success' });

	// This breaks when icon libraries update, even if visually identical
	expect(body).toContain(
		'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
	);
});

// ✅ ROBUST PATTERN - Tests semantic meaning and user experience
it('should indicate success state to users', async () => {
	render(StatusIcon, { status: 'success' });

	// Test what users actually see and experience
	await expect
		.element(page.getByRole('img', { name: /success/i }))
		.toBeInTheDocument();
	await expect
		.element(page.getByTestId('status-icon'))
		.toHaveClass('text-success');
});

Test These ✅

Semantic Classes: CSS classes that control user-visible appearance

it('should apply correct styling classes', () => {
	const { body } = render(Button, { variant: 'success' });

	expect(body).toContain('text-success'); // Color indicates success
	expect(body).toContain('btn-success'); // Semantic button class
	expect(body).toContain('px-4 py-2'); // Consistent spacing
});

User-Visible Behavior: What users actually experience

it('should respond to user interactions', async () => {
	const click_handler = vi.fn();
	render(Button, { onclick: click_handler });

	const button = page.getByRole('button');
	await button.click();

	expect(click_handler).toHaveBeenCalledOnce();
	await expect.element(button).toBeFocused();
});

Don’t Test These ❌

Exact SVG Path Coordinates: Mathematical details users don’t see

// ❌ Brittle - breaks when icon library updates
expect(body).toContain(
	'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
);

Internal Implementation Details: Library-specific markup

// ❌ Brittle - breaks when component library updates
expect(body).toContain('__svelte_component_internal_123');
expect(body).toContain('data-radix-collection-item');

Svelte 5 Runes Testing

Testing Reactive State with untrack()

import { flushSync, untrack } from 'svelte';

describe('Reactive State', () => {
	it('should handle $state and $derived correctly', () => {
		let count = $state(0);
		let doubled = $derived(count * 2);

		// ✅ Always use untrack() for $derived values
		expect(untrack(() => doubled)).toBe(0);

		count = 5;
		flushSync(); // Still needed for derived state evaluation

		expect(untrack(() => doubled)).toBe(10);
	});

	it('should test form state lifecycle', () => {
		const form_state = create_form_state({
			email: { value: '', validation_rules: { required: true } },
		});

		// ✅ Test the full lifecycle: valid → validate → invalid → fix → valid
		expect(untrack(() => form_state.is_form_valid())).toBe(true); // Initially valid

		form_state.validate_all_fields();
		expect(untrack(() => form_state.is_form_valid())).toBe(false); // Now invalid

		form_state.email.value = '[email protected]';
		expect(untrack(() => form_state.is_form_valid())).toBe(true); // Valid again
	});
});

Accessibility Testing Patterns

Semantic Queries Priority

Always prefer semantic queries that test accessibility:

describe('Accessibility Best Practices', () => {
	it('should use semantic queries for better accessibility testing', async () => {
		render(ContactForm);

		// ✅ EXCELLENT - Tests accessibility and semantics
		const name_input = page.getByRole('textbox', {
			name: 'Full Name',
		});
		const email_input = page.getByLabelText('Email address');
		const submit_button = page.getByRole('button', {
			name: 'Submit form',
		});

		await name_input.fill('John Doe');
		await email_input.fill('[email protected]');
		await submit_button.click();

		// ✅ GOOD - Tests text content users see
		await expect
			.element(page.getByText('Thank you, John!'))
			.toBeInTheDocument();
	});

	it('should test ARIA properties and roles', async () => {
		render(Modal, { open: true, title: 'Settings' });

		const modal = page.getByRole('dialog');
		await expect.element(modal).toHaveAttribute('aria-labelledby');
		await expect.element(modal).toHaveAttribute('aria-modal', 'true');

		const title = page.getByRole('heading', { level: 2 });
		await expect.element(title).toHaveText('Settings');
	});

	it('should test keyboard navigation', async () => {
		render(TabPanel);

		const first_tab = page.getByRole('tab').first();
		await first_tab.focus();

		// Test arrow key navigation
		await page.keyboard.press('ArrowRight');
		const second_tab = page.getByRole('tab').nth(1);
		await expect.element(second_tab).toBeFocused();

		// Test Enter key activation
		await page.keyboard.press('Enter');
		await expect
			.element(second_tab)
			.toHaveAttribute('aria-selected', 'true');
	});
});

Component Testing Patterns

Props and Event Testing

describe('Component Props and Events', () => {
	it('should handle all prop variants systematically', async () => {
		const variants = ['primary', 'secondary', 'danger'] as const;
		const sizes = ['sm', 'md', 'lg'] as const;

		for (const variant of variants) {
			for (const size of sizes) {
				render(Button, { variant, size });

				const button = page.getByRole('button');
				await expect.element(button).toHaveClass(`btn-${variant}`);
				await expect.element(button).toHaveClass(`btn-${size}`);
			}
		}
	});

	it('should handle multiple event types', async () => {
		const handlers = {
			click: vi.fn(),
			focus: vi.fn(),
			blur: vi.fn(),
			keydown: vi.fn(),
		};

		render(InteractiveComponent, {
			onclick: handlers.click,
			onfocus: handlers.focus,
			onblur: handlers.blur,
			onkeydown: handlers.keydown,
		});

		const element = page.getByRole('button');

		// Test click
		await element.click();
		expect(handlers.click).toHaveBeenCalledOnce();

		// Test focus/blur
		await element.focus();
		expect(handlers.focus).toHaveBeenCalledOnce();

		await element.blur();
		expect(handlers.blur).toHaveBeenCalledOnce();

		// Test keyboard
		await element.focus();
		await element.press('Enter');
		expect(handlers.keydown).toHaveBeenCalledWith(
			expect.objectContaining({ key: 'Enter' }),
		);
	});
});

Mocking Best Practices

Smart Mocking Strategy

describe('Mocking Patterns', () => {
	// ✅ Mock utility functions with realistic return values
	vi.mock('$lib/utils/data-fetcher', () => ({
		fetch_user_data: vi.fn(() =>
			Promise.resolve({
				id: '1',
				name: 'Test User',
				email: '[email protected]',
			}),
		),
		fetch_todos: vi.fn(() =>
			Promise.resolve([
				{ id: '1', title: 'Test Todo', completed: false },
			]),
		),
	}));

	it('should verify mocks are working correctly', async () => {
		const { fetch_user_data } = await import(
			'$lib/utils/data-fetcher'
		);

		expect(fetch_user_data).toBeDefined();
		expect(vi.isMockFunction(fetch_user_data)).toBe(true);

		const result = await fetch_user_data('123');
		expect(result).toEqual({
			id: '1',
			name: 'Test User',
			email: '[email protected]',
		});
	});

	it('should test component with mocked data', async () => {
		render(UserProfile, { user_id: '123' });

		// Wait for async data loading
		await expect
			.element(page.getByText('Test User'))
			.toBeInTheDocument();
		await expect
			.element(page.getByText('[email protected]'))
			.toBeInTheDocument();
	});
});

Error Handling and Edge Cases

Robust Error Testing

describe('Error Handling', () => {
	it('should handle component errors gracefully', async () => {
		// Mock console.error to avoid test noise
		const console_error = vi
			.spyOn(console, 'error')
			.mockImplementation(() => {});

		render(ErrorBoundary, {
			children: createRawSnippet(() => ({
				render: () => {
					throw new Error('Component crashed!');
				},
			})),
		});

		await expect
			.element(page.getByText('Something went wrong'))
			.toBeInTheDocument();
		await expect
			.element(page.getByRole('button', { name: 'Try again' }))
			.toBeInTheDocument();

		console_error.mockRestore();
	});

	it('should handle network failures', async () => {
		// Mock fetch to simulate network error
		vi.spyOn(global, 'fetch').mockRejectedValueOnce(
			new Error('Network error'),
		);

		render(DataComponent);

		await expect
			.element(page.getByText('Failed to load data'))
			.toBeInTheDocument();
		await expect
			.element(page.getByRole('button', { name: 'Retry' }))
			.toBeInTheDocument();
	});

	it('should handle empty data states', async () => {
		render(TodoList, { todos: [] });

		await expect
			.element(page.getByText('No todos yet'))
			.toBeInTheDocument();
		await expect
			.element(page.getByText('Add your first todo to get started'))
			.toBeInTheDocument();
	});
});

Performance and Animation Testing

Handle Animations and Timing

describe('Animation and Performance', () => {
	it('should handle animated elements', async () => {
		render(AnimatedModal, { open: true });

		const modal = page.getByRole('dialog');

		// ✅ Use force: true for elements that may be animating
		const close_button = page.getByRole('button', { name: 'Close' });
		await close_button.click({ force: true });

		// Wait for animation to complete
		await expect.element(modal).not.toBeInTheDocument();
	});

	it('should test component performance', async () => {
		const start = performance.now();

		render(ComplexDashboard, { data: large_dataset });

		// Wait for initial render
		await expect
			.element(page.getByTestId('dashboard'))
			.toBeInTheDocument();

		const render_time = performance.now() - start;
		expect(render_time).toBeLessThan(1000); // Should render within 1 second
	});
});

SSR Testing Patterns

Server-Side Rendering Validation

import { render } from 'svelte/server';

describe('SSR Testing', () => {
	it('should render without errors on server', () => {
		expect(() => {
			render(ComponentName);
		}).not.toThrow();
	});

	it('should render essential content for SEO', () => {
		const { body, head } = render(HomePage);

		// Test core content
		expect(body).toContain('<h1>Welcome to Our Site</h1>');
		expect(body).toContain('href="/about"');
		expect(body).toContain('main');

		// Test meta information
		expect(head).toContain('<title>');
		expect(head).toContain('meta name="description"');
	});

	it('should handle props correctly in SSR', () => {
		const { body } = render(UserCard, {
			user: { name: 'John Doe', email: '[email protected]' },
		});

		expect(body).toContain('John Doe');
		expect(body).toContain('[email protected]');
	});
});

Quick Reference Checklist

Essential Patterns ✅

  • Use describe and it (not test) for consistency with Vitest docs
  • Use it.skip for planned tests, not strict 100% coverage
  • Always use locators (page.getBy*()) - never containers
  • Always await locator assertions: await expect.element()
  • Use untrack() for Svelte 5 $derived values
  • Use .first(), .nth(), .last() for multiple elements
  • Use force: true for animations: await element.click({ force: true })
  • Prefer semantic queries over test IDs
  • Test user value, not implementation details
  • Use real FormData/Request objects in server tests
  • Share validation logic between client and server
  • Mock external services, keep data contracts real

Common Mistakes ❌

  • Never click SvelteKit form submits - test state directly
  • Don’t ignore strict mode violations - use .first() instead
  • Don’t test SVG paths or internal markup
  • Don’t assume element roles - verify with browser dev tools
  • Don’t write tests for arbitrary coverage metrics
  • Don’t use containers from render() - use page locators instead

Code Style Requirements

  • Use snake_case for variables and functions
  • Use kebab-case for file names
  • Prefer arrow functions where possible
  • Keep interfaces in TitleCase