This is the comprehensive testing documentation for Sveltest - vitest-browser-svelte patterns for Svelte 5.
# Sveltest Testing Documentation
> Comprehensive vitest-browser-svelte testing patterns for modern Svelte 5 applications. Real-world examples demonstrating client-server alignment, component testing in actual browsers, SSR validation, and migration from @testing-library/svelte.
# Getting Started
# Getting Started
## Learn Modern Svelte Testing
This guide goes through getting set up for testing Svelte 5
applications using the experimental `vitest-browser-svelte` - the
modern testing solution that runs your tests in real browsers instead
of simulated environments.
> **Note:** Vitest Browser Mode is currently experimental. While
> stable for most use cases, APIs may change in future versions. Pin
> your Vitest version when using Browser Mode in production.
**You'll learn:**
- Essential testing patterns that work in real browsers
- Best practices for testing Svelte 5 components with runes
- How to avoid common pitfalls and write reliable tests
- The **Client-Server Alignment Strategy** for reliable full-stack
testing
### What is Sveltest?
Sveltest is a **reference guide and example project** that
demonstrates real-world testing patterns with `vitest-browser-svelte`.
You don't install Sveltest - you learn from it and apply these
patterns to your own Svelte applications.
**Use this guide to:**
- Learn `vitest-browser-svelte` testing patterns
- Understand best practices through working code
- See comprehensive test coverage in action
### Who is Sveltest For?
- **Svelte developers** wanting to learn modern testing approaches
- **Teams** looking to establish consistent testing patterns
- **Developers migrating** from @testing-library/svelte or other
testing tools
- **Anyone** who wants to test Svelte components in real browser
environments
## Setup Your Own Project
To follow along, you'll need a Svelte project with
`vitest-browser-svelte` configured. This may _soon_ be the default,
currently (at the time of writing) it is not. To start testing
components in an actual browser using `vitest-browser-svelte` create a
new project using the `sv` CLI:
```bash
# Create a new SvelteKit project with sv
pnpm dlx sv@latest create my-testing-app
```
These are the options that will be used in these examples:
```bash
┌ Welcome to the Svelte CLI! (v0.8.7)
│
◆ Which template would you like?
│ ● SvelteKit minimal (barebones scaffolding for your new app)
│
◆ Add type checking with TypeScript?
│ ● Yes, using TypeScript syntax
│
◆ What would you like to add to your project?
│ ◼ prettier
│ ◼ eslint
│ ◼ vitest (unit testing)
│ ◼ playwright
│ ◼ tailwindcss
└
```
### Install Browser Testing Dependencies
```bash
cd my-testing-app
# Add vitest browser, Svelte testing and playwright
pnpm install -D @vitest/browser vitest-browser-svelte playwright
# remove testing library and jsdom
pnpm un @testing-library/jest-dom @testing-library/svelte jsdom
```
### Configure Vitest Browser Mode
Update your `vite.config.ts` to use the official Vitest Browser
configuration. This multi-project setup supports the **Client-Server
Alignment Strategy** - testing client components in real browsers
while keeping server tests fast with minimal mocking:
```typescript
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
test: {
projects: [
{
// Client-side tests (Svelte components)
extends: './vite.config.ts',
test: {
name: 'client',
environment: 'browser',
// Timeout for browser tests - prevent hanging on element lookups
testTimeout: 2000,
browser: {
enabled: true,
provider: 'playwright',
// Multiple browser instances for better performance
// Uses single Vite server with shared caching
instances: [
{ browser: 'chromium' },
// { browser: 'firefox' },
// { browser: 'webkit' },
],
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: [
'src/lib/server/**',
'src/**/*.ssr.{test,spec}.{js,ts}',
],
setupFiles: ['./vitest-setup-client.ts'],
},
},
{
// SSR tests (Server-side rendering)
extends: './vite.config.ts',
test: {
name: 'ssr',
environment: 'node',
include: ['src/**/*.ssr.{test,spec}.{js,ts}'],
},
},
{
// Server-side tests (Node.js utilities)
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: [
'src/**/*.svelte.{test,spec}.{js,ts}',
'src/**/*.ssr.{test,spec}.{js,ts}',
],
},
},
],
},
});
```
### Edit Setup File
Replace the contents of the `vitest-setup-client.ts` with this:
```typescript
///
///
```
### Run the tests
Running `pnpm run test:unit` on the project now is going to fail! The
`page.svelte.test.ts` file is still configured to use
`@testing-library/svelte`, replace the contents with this:
```ts
import { page } from '@vitest/browser/context';
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import Page from './+page.svelte';
describe('/+page.svelte', () => {
it('should render h1', async () => {
render(Page);
const heading = page.getByRole('heading', { level: 1 });
await expect.element(heading).toBeInTheDocument();
});
});
```
Running `pnpm run test:unit` should run the `page.svelte.test.ts` file
in the browser and pass!
## Understanding the Client-Server Alignment Strategy
Before diving into component testing, it's important to understand the
**Client-Server Alignment Strategy** that guides this testing
approach:
### The Four-Layer Approach
1. **Shared Validation Logic**: Use the same validation functions on
both client and server
2. **Real FormData/Request Objects**: Server tests use real web APIs,
not mocks
3. **TypeScript Contracts**: Shared interfaces catch mismatches at
compile time
4. **E2E Tests**: Final safety net for complete integration validation
### Why This Matters
Traditional testing with heavy mocking can pass while production fails
due to client-server mismatches. This strategy ensures your tests
catch real integration issues:
```typescript
// ❌ BRITTLE: Heavy mocking hides real issues
const mock_request = { formData: vi.fn().mockResolvedValue(...) };
// ✅ ROBUST: Real FormData catches field name mismatches
const form_data = new FormData();
form_data.append('email', 'user@example.com');
const request = new Request('http://localhost/api/register', {
method: 'POST',
body: form_data,
});
```
This multi-project Vitest setup supports this strategy by keeping
client, server, and SSR tests separate while maintaining shared
validation logic.
## Write Your First Test
Let's create a simple button component and test it step-by-step **in
your own project**.
### Step 1: Create a Simple Component
Create `src/lib/components/my-button.svelte`:
```svelte
```
### Step 2: Write Your First Test
Create `src/lib/components/my-button.svelte.test.ts`:
```typescript
import { describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { page } from '@vitest/browser/context';
import { createRawSnippet } from 'svelte';
import MyButton from './my-button.svelte';
describe('MyButton', () => {
it('should render with correct text', async () => {
const children = createRawSnippet(() => ({
render: () => `Click me`,
}));
render(MyButton, { children });
const button = page.getByRole('button', { name: 'Click me' });
await expect.element(button).toBeInTheDocument();
});
it('should handle click events', async () => {
const click_handler = vi.fn();
const children = createRawSnippet(() => ({
render: () => `Click me`,
}));
render(MyButton, { onclick: click_handler, children });
const button = page.getByRole('button', { name: 'Click me' });
await button.click();
expect(click_handler).toHaveBeenCalledOnce();
});
it('should apply correct variant class', async () => {
const children = createRawSnippet(() => ({
render: () => `Secondary`,
}));
render(MyButton, { variant: 'secondary', children });
const button = page.getByTestId('my-button');
await expect.element(button).toHaveClass('btn-secondary');
});
});
```
Time to test it out!
### Step 3: Run Your Test
If you already have `pnpm run test:unit` running it should update in
watch mode!
You can test on a component basis too, this is handy if you have a lot
of tests and want to isolate what you're testing:
```bash
# run once
pnpm vitest run src/lib/components/my-button.svelte
# use watch mode
pnpm vitest src/lib/components/my-button.svelte
```
You should see all tests pass! 🎉
## Understanding the Test Structure
Let's break down what makes this test work with Vitest Browser Mode:
### Essential Imports
```typescript
import { describe, expect, it, vi } from 'vitest'; // Test framework
import { render } from 'vitest-browser-svelte'; // Svelte rendering
import { page } from '@vitest/browser/context'; // Browser interactions
import { createRawSnippet } from 'svelte'; // Svelte 5 snippets
```
### The Golden Rule: Always Use Locators
Following the official Vitest Browser documentation, **always use
locators** for reliable, auto-retrying queries:
```typescript
// ✅ DO: Use page locators (auto-retry, semantic)
const button = page.getByRole('button', { name: 'Click me' });
await button.click();
// ❌ DON'T: Use containers (no auto-retry, manual queries)
const { container } = render(MyButton);
const button = container.querySelector('button');
```
### Locator Hierarchy (Use in This Order)
Following Vitest Browser best practices:
1. **Semantic roles** (best for accessibility):
```typescript
page.getByRole('button', { name: 'Submit' });
page.getByRole('textbox', { name: 'Email' });
```
2. **Labels** (good for forms):
```typescript
page.getByLabel('Email address');
```
3. **Text content** (good for unique text):
```typescript
page.getByText('Welcome back');
```
4. **Test IDs** (fallback for complex cases):
```typescript
page.getByTestId('submit-button');
```
### Critical: Handle Multiple Elements
Vitest Browser operates in **strict mode** - if multiple elements
match, you'll get an error:
```typescript
// ❌ FAILS: "strict mode violation" if multiple elements match
page.getByRole('link', { name: 'Home' });
// ✅ CORRECT: Use .first(), .nth(), .last() for multiple elements
page.getByRole('link', { name: 'Home' }).first();
page.getByRole('link', { name: 'Home' }).nth(1); // Second element (0-indexed)
page.getByRole('link', { name: 'Home' }).last();
```
## Common Patterns You'll Use Daily
### Testing Form Inputs
```typescript
it('should handle form input', async () => {
render(MyInput, { label: 'Email', type: 'email' });
const input = page.getByLabel('Email');
await input.fill('user@example.com');
await expect.element(input).toHaveValue('user@example.com');
});
```
### Testing Conditional Rendering
```typescript
it('should show error message when invalid', async () => {
render(MyInput, {
label: 'Email',
error: 'Invalid email format',
});
await expect
.element(page.getByText('Invalid email format'))
.toBeInTheDocument();
});
```
### Testing Loading States
```typescript
it('should show loading state', async () => {
const children = createRawSnippet(() => ({
render: () => `Loading...`,
}));
render(MyButton, { loading: true, children });
await expect.element(page.getByRole('button')).toBeDisabled();
await expect
.element(page.getByText('Loading...'))
.toBeInTheDocument();
});
```
### Testing Svelte 5 Runes
Use `untrack()` when testing derived state:
```typescript
import { untrack, flushSync } from 'svelte';
it('should handle reactive state', () => {
let count = $state(0);
let doubled = $derived(count * 2);
expect(untrack(() => doubled)).toBe(0);
count = 5;
flushSync(); // Ensure derived state updates
expect(untrack(() => doubled)).toBe(10);
});
```
## Quick Wins: Copy These Patterns
### The Foundation First Template
Start every component test with this structure:
```typescript
describe('ComponentName', () => {
describe('Initial Rendering', () => {
it('should render with default props', async () => {
// Your first test here
});
it.skip('should render with all prop variants', async () => {
// TODO: Test different prop combinations
});
});
describe('User Interactions', () => {
it.skip('should handle click events', async () => {
// TODO: Test user interactions
});
});
describe('Edge Cases', () => {
it.skip('should handle empty data gracefully', async () => {
// TODO: Test edge cases
});
});
});
```
### The Mock Verification Pattern
Always verify your mocks work:
```typescript
describe('Mock Verification', () => {
it('should have utility functions mocked correctly', async () => {
const { my_util_function } = await import('$lib/utils/my-utils');
expect(my_util_function).toBeDefined();
expect(vi.isMockFunction(my_util_function)).toBe(true);
});
});
```
### The Accessibility Test Pattern
```typescript
it('should be accessible', async () => {
const children = createRawSnippet(() => ({
render: () => `Submit`,
}));
render(MyComponent, { children });
const button = page.getByRole('button', { name: 'Submit' });
await expect.element(button).toHaveAttribute('aria-label');
// Test keyboard navigation
await page.keyboard.press('Tab');
await expect.element(button).toBeFocused();
});
```
## Common First-Day Issues
### "strict mode violation: getByRole() resolved to X elements"
**Most common issue** with Vitest Browser Mode. Multiple elements
match your locator:
```typescript
// ❌ FAILS: Multiple nav links (desktop + mobile)
page.getByRole('link', { name: 'Home' });
// ✅ WORKS: Target specific element
page.getByRole('link', { name: 'Home' }).first();
```
### "My test is hanging, what's wrong?"
Usually caused by clicking form submit buttons with SvelteKit enhance.
Test form state directly:
```typescript
// ❌ Can hang with SvelteKit forms
await submit_button.click();
// ✅ Test the state directly
render(MyForm, { errors: { email: 'Required' } });
await expect.element(page.getByText('Required')).toBeInTheDocument();
```
### "Expected 2 arguments, but got 0"
Your mock function signature doesn't match the real function:
```typescript
// ❌ Wrong signature
vi.mock('$lib/utils', () => ({
my_function: vi.fn(),
}));
// ✅ Correct signature
vi.mock('$lib/utils', () => ({
my_function: vi.fn((param1: string, param2: number) => 'result'),
}));
```
### Role and Element Confusion
```typescript
// ❌ WRONG: Looking for link when element has role="button"
page.getByRole('link', { name: 'Submit' }); // Submit
// ✅ CORRECT: Use the actual role
page.getByRole('button', { name: 'Submit' });
// ❌ WRONG: Input role doesn't exist
page.getByRole('input', { name: 'Email' });
// ✅ CORRECT: Use textbox for input elements
page.getByRole('textbox', { name: 'Email' });
```
### Explore the Examples (Optional)
Want to see these patterns in action? Clone the Sveltest repository:
```bash
# Clone to explore examples
git clone https://github.com/spences10/sveltest.git
cd sveltest
pnpm install
# Run the example tests
pnpm test:unit
```
## What's Next?
Now that you've written your first test with Vitest Browser Mode,
explore these areas:
1. **[Testing Patterns](/docs/testing-patterns)** - Learn component,
SSR, and server testing patterns
2. **[Best Practices](/docs/best-practices)** - Master the Foundation
First approach and avoid common pitfalls
3. **[API Reference](/docs/api-reference)** - Complete reference for
all testing utilities
4. **[Migration Guide](/docs/migration-guide)** - If you're coming
from @testing-library/svelte
## Ready to Level Up?
You now have the foundation to write effective tests with Vitest
Browser Mode and `vitest-browser-svelte`. The patterns you've learned
here scale from simple buttons to complex applications.
**Next Steps:**
- Explore the [component examples](/components) to see these patterns
in action
- Check out the [todo application](/todos) for a complete testing
example
- Review the comprehensive [testing rules](/.cursor/rules/testing.mdc)
for advanced patterns
Happy testing! 🧪✨
# Testing Patterns
# 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](./best-practices.md). For setup and configuration,
see [Getting Started](./getting-started.md).
## Essential Setup Pattern
Every component test file should start with this setup:
```typescript
import { describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { page } from '@vitest/browser/context';
import { createRawSnippet } from 'svelte';
import { flushSync, untrack } from 'svelte';
// Import your component
import MyComponent from './my-component.svelte';
```
## Locator Patterns
### Basic Locator Usage
```typescript
it('should use semantic locators', async () => {
render(MyComponent);
// ✅ Semantic queries (preferred - test accessibility)
const submit_button = page.getByRole('button', { name: 'Submit' });
const email_input = page.getByRole('textbox', { name: 'Email' });
const email_label = page.getByLabel('Email address');
const welcome_text = page.getByText('Welcome');
// ✅ Test IDs (when semantic queries aren't possible)
const complex_widget = page.getByTestId('data-visualization');
// ✅ Always await assertions
await expect.element(submit_button).toBeInTheDocument();
await expect.element(email_input).toHaveAttribute('type', 'email');
});
```
### Handling Multiple Elements (Strict Mode)
vitest-browser-svelte operates in strict mode - if multiple elements
match, you must specify which one:
```typescript
it('should handle multiple matching elements', async () => {
render(NavigationComponent);
// ❌ FAILS: Strict mode violation if desktop + mobile nav both exist
// page.getByRole('link', { name: 'Home' });
// ✅ CORRECT: Use .first(), .nth(), or .last()
const desktop_home_link = page
.getByRole('link', { name: 'Home' })
.first();
const mobile_home_link = page
.getByRole('link', { name: 'Home' })
.last();
const second_link = page.getByRole('link', { name: 'Home' }).nth(1);
await expect.element(desktop_home_link).toBeInTheDocument();
await expect.element(mobile_home_link).toBeInTheDocument();
});
```
### Role Confusion Fixes
Common role mistakes and their solutions:
```typescript
it('should use correct element roles', async () => {
render(FormComponent);
// ❌ WRONG: Input role doesn't exist
// page.getByRole('input', { name: 'Email' });
// ✅ CORRECT: Use textbox for input elements
const email_input = page.getByRole('textbox', { name: 'Email' });
// ❌ WRONG: Looking for link when element has role="button"
// page.getByRole('link', { name: 'Submit' }); // Submit
// ✅ CORRECT: Use the actual role attribute
const submit_link_button = page.getByRole('button', {
name: 'Submit',
});
await expect.element(email_input).toBeInTheDocument();
await expect.element(submit_link_button).toBeInTheDocument();
});
```
## Component Testing Patterns
### Button Component Pattern
```typescript
describe('Button Component', () => {
it('should render with variant styling', async () => {
render(Button, { variant: 'primary', children: 'Click me' });
const button = page.getByRole('button', { name: 'Click me' });
await expect.element(button).toBeInTheDocument();
await expect.element(button).toHaveClass('btn-primary');
});
it('should handle click events', async () => {
const click_handler = vi.fn();
render(Button, { onclick: click_handler, children: 'Click me' });
const button = page.getByRole('button', { name: 'Click me' });
await button.click();
expect(click_handler).toHaveBeenCalledOnce();
});
it('should support disabled state', async () => {
render(Button, { disabled: true, children: 'Disabled' });
const button = page.getByRole('button', { name: 'Disabled' });
await expect.element(button).toBeDisabled();
await expect.element(button).toHaveClass('btn-disabled');
});
it('should handle animations with force click', async () => {
render(AnimatedButton, { children: 'Animated' });
const button = page.getByRole('button', { name: 'Animated' });
// Use force: true for elements that may be animating
await button.click({ force: true });
await expect
.element(page.getByText('Animation complete'))
.toBeInTheDocument();
});
});
```
### Input Component Pattern
```typescript
describe('Input Component', () => {
it('should handle user input', async () => {
render(Input, { type: 'text', label: 'Full Name' });
const input = page.getByLabelText('Full Name');
await input.fill('John Doe');
await expect.element(input).toHaveValue('John Doe');
});
it('should display validation errors', async () => {
render(Input, {
type: 'email',
label: 'Email',
error: 'Invalid email format',
});
const input = page.getByLabelText('Email');
const error_message = page.getByText('Invalid email format');
await expect.element(error_message).toBeInTheDocument();
await expect
.element(input)
.toHaveAttribute('aria-invalid', 'true');
await expect.element(input).toHaveClass('input-error');
});
it('should support different input types', async () => {
render(Input, { type: 'password', label: 'Password' });
const input = page.getByLabelText('Password');
await expect.element(input).toHaveAttribute('type', 'password');
});
});
```
### Modal Component Pattern
```typescript
describe('Modal Component', () => {
it('should handle focus management', async () => {
render(Modal, { open: true, children: 'Modal content' });
const modal = page.getByRole('dialog');
await expect.element(modal).toBeInTheDocument();
// Test focus trap
await page.keyboard.press('Tab');
const close_button = page.getByRole('button', { name: 'Close' });
await expect.element(close_button).toBeFocused();
});
it('should close on escape key', async () => {
const close_handler = vi.fn();
render(Modal, { open: true, onclose: close_handler });
await page.keyboard.press('Escape');
expect(close_handler).toHaveBeenCalledOnce();
});
it('should prevent background scroll when open', async () => {
render(Modal, { open: true });
const body = page.locator('body');
await expect.element(body).toHaveClass('modal-open');
});
});
```
### Dropdown/Select Component Pattern
```typescript
describe('Dropdown Component', () => {
it('should open and close on click', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(Dropdown, { options, label: 'Choose option' });
const trigger = page.getByRole('button', {
name: 'Choose option',
});
await trigger.click();
// Dropdown should be open
const option1 = page.getByRole('option', { name: 'Option 1' });
await expect.element(option1).toBeInTheDocument();
// Select an option
await option1.click();
// Dropdown should close and show selected value
await expect.element(trigger).toHaveTextContent('Option 1');
});
it('should support keyboard navigation', async () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(Dropdown, { options, label: 'Choose option' });
const trigger = page.getByRole('button', {
name: 'Choose option',
});
await trigger.focus();
await page.keyboard.press('Enter');
// Navigate with arrow keys
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect.element(trigger).toHaveTextContent('Option 1');
});
});
```
## Svelte 5 Runes Testing Patterns
### $state and $derived Testing
```typescript
describe('Reactive State Component', () => {
it('should handle $state updates', async () => {
render(CounterComponent);
const count_display = page.getByTestId('count');
const increment_button = page.getByRole('button', {
name: 'Increment',
});
// Initial state
await expect.element(count_display).toHaveTextContent('0');
// Update state
await increment_button.click();
await expect.element(count_display).toHaveTextContent('1');
});
it('should handle $derived values with untrack', () => {
let count = $state(0);
let doubled = $derived(count * 2);
// ✅ Always use untrack() when accessing $derived values
expect(untrack(() => doubled)).toBe(0);
count = 5;
flushSync(); // Ensure derived state is evaluated
expect(untrack(() => doubled)).toBe(10);
});
it('should handle $derived from object getters', () => {
const state_object = {
get computed_value() {
return $derived(() => some_calculation());
},
};
// ✅ Get the $derived function first, then use untrack
const derived_fn = state_object.computed_value;
expect(untrack(() => derived_fn())).toBe(expected_value);
});
});
```
### Real-World Untrack Examples
#### Testing Form State with Multiple $derived Values
```typescript
// From form-state.test.ts - Testing complex derived state
describe('Form State Derived Values', () => {
it('should validate form state correctly', () => {
const form = create_form_state({
email: { value: '', validation_rules: { required: true } },
password: {
value: '',
validation_rules: { required: true, min_length: 8 },
},
});
// Test initial state
expect(untrack(() => form.is_form_valid())).toBe(true);
expect(untrack(() => form.has_changes())).toBe(false);
expect(untrack(() => form.field_errors())).toEqual({});
// Update field and test derived state changes
form.update_field('email', 'invalid');
flushSync();
expect(untrack(() => form.is_form_valid())).toBe(false);
expect(untrack(() => form.has_changes())).toBe(true);
const errors = untrack(() => form.field_errors());
expect(errors.email).toBe('Invalid format');
});
});
```
#### Testing Calculator State Transitions
```typescript
// From calculator.test.ts - Testing state getters
describe('Calculator State Management', () => {
it('should handle calculator state transitions', () => {
// Test initial state
expect(untrack(() => calculator_state.current_value)).toBe('0');
expect(untrack(() => calculator_state.previous_value)).toBe('');
expect(untrack(() => calculator_state.operation)).toBe('');
expect(untrack(() => calculator_state.waiting_for_operand)).toBe(
false,
);
// Perform operation and test state changes
calculator_state.input_digit('5');
calculator_state.input_operation('+');
flushSync();
expect(untrack(() => calculator_state.current_value)).toBe('5');
expect(untrack(() => calculator_state.operation)).toBe('+');
expect(untrack(() => calculator_state.waiting_for_operand)).toBe(
true,
);
});
});
```
#### ✅ VALIDATED: Creating $derived State in Tests
**Key Discovery**: Runes can only be used in `.test.svelte.ts` files,
not regular `.ts` files!
```typescript
// From untrack-validation.test.svelte.ts - PROVEN WORKING PATTERN
describe('Untrack Usage Validation', () => {
it('should access $derived values using untrack', () => {
// ✅ Create reactive state directly in test (.test.svelte.ts file)
let email = $state('');
const email_validation = $derived(validate_email(email));
// Test invalid email
email = 'invalid-email';
flushSync();
// ✅ CORRECT: Use untrack to access $derived value
const result = untrack(() => email_validation);
expect(result.is_valid).toBe(false);
expect(result.error_message).toBe('Invalid format');
// Test valid email
email = 'test@example.com';
flushSync();
const valid_result = untrack(() => email_validation);
expect(valid_result.is_valid).toBe(true);
expect(valid_result.error_message).toBe('');
});
it('should handle complex derived logic', () => {
// ✅ Recreate component logic in test
let email = $state('');
let submit_attempted = $state(false);
let email_touched = $state(false);
const email_validation = $derived(validate_email(email));
const show_email_error = $derived(
submit_attempted || email_touched,
);
const email_error = $derived(
show_email_error && !email_validation.is_valid
? email_validation.error_message
: '',
);
// Initially no errors shown
expect(untrack(() => show_email_error)).toBe(false);
expect(untrack(() => email_error)).toBe('');
// After touching field with invalid email
email = 'invalid';
email_touched = true;
flushSync();
expect(untrack(() => show_email_error)).toBe(true);
expect(untrack(() => email_error)).toBe('Invalid format');
});
it('should test state transitions with untrack', () => {
// ✅ Test reactive state changes
let count = $state(0);
let doubled = $derived(count * 2);
let is_even = $derived(count % 2 === 0);
// Initial state
expect(untrack(() => count)).toBe(0);
expect(untrack(() => doubled)).toBe(0);
expect(untrack(() => is_even)).toBe(true);
// Update state
count = 3;
flushSync();
// Test all derived values
expect(untrack(() => count)).toBe(3);
expect(untrack(() => doubled)).toBe(6);
expect(untrack(() => is_even)).toBe(false);
});
it('should handle form validation patterns', () => {
// ✅ Recreate login form validation logic
let email = $state('');
let password = $state('');
let loading = $state(false);
const email_validation = $derived(validate_email(email));
const password_validation = $derived(validate_password(password));
const form_is_valid = $derived(
email_validation.is_valid && password_validation.is_valid,
);
const can_submit = $derived(form_is_valid && !loading);
// Test form validation chain
email = 'test@example.com';
password = 'ValidPassword123';
flushSync();
expect(untrack(() => email_validation.is_valid)).toBe(true);
expect(untrack(() => password_validation.is_valid)).toBe(true);
expect(untrack(() => form_is_valid)).toBe(true);
expect(untrack(() => can_submit)).toBe(true);
// Test loading state
loading = true;
flushSync();
expect(untrack(() => can_submit)).toBe(false);
});
});
```
#### Testing Component $derived Values (Theoretical)
```typescript
// NOTE: This pattern requires component internals to be exposed
// Currently not possible with Svelte 5 component encapsulation
describe('LoginForm Derived State', () => {
it('should validate email and calculate form validity', async () => {
const { component } = render(LoginForm);
// ❌ This doesn't work - component internals not exposed
// component.email = 'invalid-email';
// expect(untrack(() => component.email_validation)).toBe(...);
// ✅ Instead, test through UI interactions
const email_input = page.getByLabelText('Email');
await email_input.fill('invalid-email');
await email_input.blur();
await expect
.element(page.getByText('Invalid format'))
.toBeInTheDocument();
});
});
```
### Form Validation Lifecycle Pattern
```typescript
describe('Form Validation Component', () => {
it('should follow validation lifecycle', () => {
const form_state = create_form_state({
email: {
value: '',
validation_rules: { required: true },
},
});
// ✅ CORRECT: Forms typically start valid (not validated yet)
const is_form_valid = form_state.is_form_valid;
expect(untrack(() => is_form_valid())).toBe(true);
// Trigger validation - now should be invalid
form_state.validate_all_fields();
flushSync();
expect(untrack(() => is_form_valid())).toBe(false);
// Fix the field - should become valid again
form_state.update_field('email', 'valid@example.com');
flushSync();
expect(untrack(() => is_form_valid())).toBe(true);
});
it('should handle field-level validation', async () => {
render(FormComponent);
const email_input = page.getByLabelText('Email');
// Initially no error
await expect
.element(page.getByText('Email is required'))
.not.toBeInTheDocument();
// Trigger validation by focusing and blurring
await email_input.focus();
await email_input.blur();
// Error should appear
await expect
.element(page.getByText('Email is required'))
.toBeInTheDocument();
// Fix the error
await email_input.fill('valid@example.com');
await email_input.blur();
// Error should disappear
await expect
.element(page.getByText('Email is required'))
.not.toBeInTheDocument();
});
});
```
## Integration Testing Patterns
### Form Submission Pattern
```typescript
describe('Contact Form Integration', () => {
it('should handle complete form submission flow', async () => {
const submit_handler = vi.fn();
render(ContactForm, { onsubmit: submit_handler });
// Fill out form
const name_input = page.getByLabelText('Name');
const email_input = page.getByLabelText('Email');
const message_input = page.getByLabelText('Message');
await name_input.fill('John Doe');
await email_input.fill('john@example.com');
await message_input.fill('Hello world');
// Submit form
const submit_button = page.getByRole('button', {
name: 'Send Message',
});
await submit_button.click();
// Verify submission
expect(submit_handler).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
message: 'Hello world',
});
// Verify success message
await expect
.element(page.getByText('Message sent successfully'))
.toBeInTheDocument();
});
it('should prevent submission with invalid data', async () => {
const submit_handler = vi.fn();
render(ContactForm, { onsubmit: submit_handler });
// Try to submit empty form
const submit_button = page.getByRole('button', {
name: 'Send Message',
});
await submit_button.click();
// Should show validation errors
await expect
.element(page.getByText('Name is required'))
.toBeInTheDocument();
await expect
.element(page.getByText('Email is required'))
.toBeInTheDocument();
// Should not call submit handler
expect(submit_handler).not.toHaveBeenCalled();
});
});
```
### Todo List Pattern
```typescript
describe('Todo List Integration', () => {
it('should handle complete todo lifecycle', async () => {
render(TodoManager);
// Add todo
const input = page.getByLabelText('New todo');
await input.fill('Buy groceries');
const add_button = page.getByRole('button', { name: 'Add Todo' });
await add_button.click();
// Verify todo appears
const todo_item = page.getByText('Buy groceries');
await expect.element(todo_item).toBeInTheDocument();
// Complete todo
const checkbox = page.getByRole('checkbox', {
name: 'Mark Buy groceries as complete',
});
await checkbox.check();
// Verify completion styling
await expect.element(checkbox).toBeChecked();
await expect.element(todo_item).toHaveClass('todo-completed');
// Delete todo
const delete_button = page.getByRole('button', {
name: 'Delete Buy groceries',
});
await delete_button.click();
// Verify removal
await expect.element(todo_item).not.toBeInTheDocument();
});
});
```
### Navigation Pattern
```typescript
describe('Navigation Integration', () => {
it('should navigate between pages', async () => {
render(AppLayout);
// Navigate to docs
const docs_link = page
.getByRole('link', { name: 'Documentation' })
.first();
await docs_link.click();
await expect
.element(page.getByText('Getting Started'))
.toBeInTheDocument();
// Navigate to examples
const examples_link = page
.getByRole('link', { name: 'Examples' })
.first();
await examples_link.click();
await expect
.element(page.getByText('Example Components'))
.toBeInTheDocument();
});
it('should highlight active navigation', async () => {
render(AppLayout, { current_page: '/docs' });
const docs_link = page
.getByRole('link', { name: 'Documentation' })
.first();
await expect.element(docs_link).toHaveClass('nav-active');
const home_link = page
.getByRole('link', { name: 'Home' })
.first();
await expect.element(home_link).not.toHaveClass('nav-active');
});
});
```
## SSR Testing Patterns
### Basic SSR Pattern
```typescript
import { render } from 'svelte/server';
import { describe, expect, test } from 'vitest';
describe('Component SSR', () => {
it('should render without errors', () => {
expect(() => {
render(ComponentName);
}).not.toThrow();
});
it('should render essential content for SEO', () => {
const { body } = render(ComponentName, {
props: { title: 'Page Title', description: 'Page description' },
});
expect(body).toContain('Page Title
');
expect(body).toContain('Page description');
expect(body).toContain('href="/important-link"');
});
it('should render meta information', () => {
const { head } = render(ComponentName, {
props: { title: 'Page Title' },
});
expect(head).toContain('Page Title');
expect(head).toContain('meta name="description"');
});
});
```
### Layout SSR Pattern
```typescript
describe('Layout SSR', () => {
it('should render navigation structure', () => {
const { body } = render(Layout);
expect(body).toContain('