Files
Job-Info/svelte-reference/09-TESTING.md
T
aewing 0b76a4b119 Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test)
- Delete old documentation, Docker files, and k8s configs
- Add comprehensive Svelte 5 reference library (svelte-reference/)
- Update ModeSwitch orchestrator:
  - Fix port to 3005 (never use 3000/3001)
  - Add API endpoints for mode switching
  - Add bun-types and proper tsconfig
  - Clean up mode folder mappings
2026-01-21 03:56:01 +00:00

16 KiB

Svelte Testing Guide

Overview

Testing Svelte applications involves unit testing components, integration testing with stores/context, and end-to-end testing with real browsers.


🔧 Testing Setup

Install Dependencies

# Vitest for unit/integration tests
npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom

# Playwright for E2E tests
npm install -D @playwright/test
npx playwright install

vitest.config.ts

import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte({ hot: !process.env.VITEST })],
  test: {
    include: ['src/**/*.{test,spec}.{js,ts}'],
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/tests/setup.ts'],
    coverage: {
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'src/tests/']
    }
  }
});

Test Setup File

// src/tests/setup.ts
import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest';

// Mock SvelteKit modules
vi.mock('$app/navigation', () => ({
  goto: vi.fn(),
  invalidate: vi.fn(),
  invalidateAll: vi.fn(),
  preloadData: vi.fn(),
  preloadCode: vi.fn()
}));

vi.mock('$app/stores', () => ({
  page: {
    subscribe: vi.fn()
  },
  navigating: {
    subscribe: vi.fn()
  }
}));

vi.mock('$app/environment', () => ({
  browser: true,
  dev: true,
  building: false
}));

🧪 Unit Testing Components

Basic Component Test

// src/lib/components/Button.test.ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import Button from './Button.svelte';

describe('Button', () => {
  it('renders with text', () => {
    render(Button, { props: { children: 'Click me' } });
    
    expect(screen.getByRole('button')).toHaveTextContent('Click me');
  });
  
  it('applies variant class', () => {
    render(Button, { 
      props: { 
        variant: 'primary',
        children: 'Primary'
      } 
    });
    
    expect(screen.getByRole('button')).toHaveClass('btn-primary');
  });
  
  it('handles click events', async () => {
    const onclick = vi.fn();
    
    render(Button, { 
      props: { 
        onclick,
        children: 'Click'
      } 
    });
    
    await fireEvent.click(screen.getByRole('button'));
    
    expect(onclick).toHaveBeenCalledOnce();
  });
  
  it('is disabled when loading', () => {
    render(Button, { 
      props: { 
        loading: true,
        children: 'Submit'
      } 
    });
    
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Testing with Snippets (Svelte 5)

// src/lib/components/Card.test.ts
import { render, screen } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Card from './Card.svelte';

// Helper to create snippet-like content
const createCardWithContent = (title: string, content: string) => {
  // For Svelte 5, you may need a wrapper component
  return {
    component: Card,
    props: {
      title
    },
    // Slot content for testing
    slots: {
      default: content
    }
  };
};

describe('Card', () => {
  it('renders title and content', () => {
    render(Card, {
      props: { title: 'Card Title' }
      // Note: Testing snippets may require wrapper components
    });
    
    expect(screen.getByText('Card Title')).toBeInTheDocument();
  });
});

Testing Reactive State

// src/lib/components/Counter.test.ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';

describe('Counter', () => {
  it('increments count on click', async () => {
    render(Counter);
    
    const button = screen.getByRole('button', { name: /increment/i });
    const display = screen.getByTestId('count');
    
    expect(display).toHaveTextContent('0');
    
    await fireEvent.click(button);
    
    expect(display).toHaveTextContent('1');
    
    await fireEvent.click(button);
    await fireEvent.click(button);
    
    expect(display).toHaveTextContent('3');
  });
  
  it('starts with initial value', () => {
    render(Counter, { props: { initialValue: 10 } });
    
    expect(screen.getByTestId('count')).toHaveTextContent('10');
  });
});

Testing Async Components

// src/lib/components/UserProfile.test.ts
import { render, screen, waitFor } from '@testing-library/svelte';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import UserProfile from './UserProfile.svelte';

// Mock fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;

describe('UserProfile', () => {
  beforeEach(() => {
    mockFetch.mockReset();
  });
  
  it('shows loading state', () => {
    mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves
    
    render(UserProfile, { props: { userId: '1' } });
    
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });
  
  it('displays user data after loading', async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve({ name: 'John Doe', email: 'john@example.com' })
    });
    
    render(UserProfile, { props: { userId: '1' } });
    
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
    
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });
  
  it('shows error on fetch failure', async () => {
    mockFetch.mockRejectedValueOnce(new Error('Network error'));
    
    render(UserProfile, { props: { userId: '1' } });
    
    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent('Failed to load user');
    });
  });
});

🏪 Testing Stores

Testing .svelte.ts Stores

// src/lib/stores/counter.svelte.ts
export class Counter {
  count = $state(0);
  doubled = $derived(this.count * 2);
  
  increment() {
    this.count++;
  }
  
  decrement() {
    this.count--;
  }
  
  reset() {
    this.count = 0;
  }
}

// src/lib/stores/counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { Counter } from './counter.svelte';

describe('Counter Store', () => {
  let counter: Counter;
  
  beforeEach(() => {
    counter = new Counter();
  });
  
  it('starts at 0', () => {
    expect(counter.count).toBe(0);
  });
  
  it('increments', () => {
    counter.increment();
    expect(counter.count).toBe(1);
    
    counter.increment();
    counter.increment();
    expect(counter.count).toBe(3);
  });
  
  it('decrements', () => {
    counter.count = 5;
    counter.decrement();
    expect(counter.count).toBe(4);
  });
  
  it('computes doubled value', () => {
    counter.count = 5;
    expect(counter.doubled).toBe(10);
    
    counter.increment();
    expect(counter.doubled).toBe(12);
  });
  
  it('resets to 0', () => {
    counter.count = 100;
    counter.reset();
    expect(counter.count).toBe(0);
  });
});

Testing Async Stores

// src/lib/stores/users.svelte.ts
export class UsersStore {
  users = $state<User[]>([]);
  loading = $state(false);
  error = $state<string | null>(null);
  
  async fetchUsers() {
    this.loading = true;
    this.error = null;
    
    try {
      const response = await fetch('/api/users');
      if (!response.ok) throw new Error('Failed to fetch');
      this.users = await response.json();
    } catch (e) {
      this.error = e instanceof Error ? e.message : 'Unknown error';
    } finally {
      this.loading = false;
    }
  }
}

// src/lib/stores/users.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UsersStore } from './users.svelte';

const mockFetch = vi.fn();
global.fetch = mockFetch;

describe('UsersStore', () => {
  let store: UsersStore;
  
  beforeEach(() => {
    store = new UsersStore();
    mockFetch.mockReset();
  });
  
  it('fetches users successfully', async () => {
    const mockUsers = [
      { id: '1', name: 'John' },
      { id: '2', name: 'Jane' }
    ];
    
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve(mockUsers)
    });
    
    await store.fetchUsers();
    
    expect(store.users).toEqual(mockUsers);
    expect(store.loading).toBe(false);
    expect(store.error).toBeNull();
  });
  
  it('handles fetch error', async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 500
    });
    
    await store.fetchUsers();
    
    expect(store.users).toEqual([]);
    expect(store.error).toBe('Failed to fetch');
  });
  
  it('sets loading state during fetch', async () => {
    mockFetch.mockImplementation(() => new Promise(resolve => {
      setTimeout(() => resolve({
        ok: true,
        json: () => Promise.resolve([])
      }), 100);
    }));
    
    const promise = store.fetchUsers();
    
    expect(store.loading).toBe(true);
    
    await promise;
    
    expect(store.loading).toBe(false);
  });
});

🔗 Integration Testing

Testing Components with Context

// src/lib/components/ThemeConsumer.test.ts
import { render, screen } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import ThemeProvider from './ThemeProvider.svelte';
import ThemeConsumer from './ThemeConsumer.svelte';

// Wrapper component for testing
import TestWrapper from './TestWrapper.svelte';

describe('ThemeConsumer', () => {
  it('uses theme from provider', () => {
    // TestWrapper.svelte wraps children in ThemeProvider
    render(TestWrapper, {
      props: {
        component: ThemeConsumer,
        theme: 'dark'
      }
    });
    
    expect(screen.getByTestId('theme-display')).toHaveTextContent('dark');
  });
});

Testing Form Submissions

// src/lib/components/LoginForm.test.ts
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import LoginForm from './LoginForm.svelte';

describe('LoginForm', () => {
  it('submits form with credentials', async () => {
    const onsubmit = vi.fn();
    
    render(LoginForm, { props: { onsubmit } });
    
    await fireEvent.input(
      screen.getByLabelText('Email'),
      { target: { value: 'test@example.com' } }
    );
    
    await fireEvent.input(
      screen.getByLabelText('Password'),
      { target: { value: 'password123' } }
    );
    
    await fireEvent.click(screen.getByRole('button', { name: /login/i }));
    
    expect(onsubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    });
  });
  
  it('shows validation errors', async () => {
    render(LoginForm);
    
    await fireEvent.click(screen.getByRole('button', { name: /login/i }));
    
    expect(screen.getByText('Email is required')).toBeInTheDocument();
    expect(screen.getByText('Password is required')).toBeInTheDocument();
  });
  
  it('disables button while loading', async () => {
    render(LoginForm, { props: { loading: true } });
    
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

🌐 E2E Testing with Playwright

playwright.config.ts

import type { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
  webServer: {
    command: 'npm run build && npm run preview',
    port: 4173
  },
  testDir: 'tests',
  testMatch: /(.+\.)?(test|spec)\.[jt]s/,
  use: {
    baseURL: 'http://localhost:4173',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure'
  },
  projects: [
    {
      name: 'chromium',
      use: { browserName: 'chromium' }
    },
    {
      name: 'firefox',
      use: { browserName: 'firefox' }
    },
    {
      name: 'webkit',
      use: { browserName: 'webkit' }
    }
  ]
};

export default config;

E2E Test Examples

// tests/auth.test.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('user can log in', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    
    // Should redirect to dashboard
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('h1')).toContainText('Dashboard');
  });
  
  test('shows error for invalid credentials', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('input[name="email"]', 'wrong@example.com');
    await page.fill('input[name="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');
    
    await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials');
    await expect(page).toHaveURL('/login');
  });
  
  test('protected routes redirect to login', async ({ page }) => {
    await page.goto('/dashboard');
    
    await expect(page).toHaveURL('/login');
  });
});

// tests/navigation.test.ts
test.describe('Navigation', () => {
  test('main navigation works', async ({ page }) => {
    await page.goto('/');
    
    await page.click('a[href="/about"]');
    await expect(page).toHaveURL('/about');
    await expect(page.locator('h1')).toContainText('About');
    
    await page.click('a[href="/contact"]');
    await expect(page).toHaveURL('/contact');
    await expect(page.locator('h1')).toContainText('Contact');
  });
  
  test('back button works', async ({ page }) => {
    await page.goto('/');
    await page.click('a[href="/about"]');
    await page.goBack();
    
    await expect(page).toHaveURL('/');
  });
});

// tests/forms.test.ts
test.describe('Contact Form', () => {
  test('submits form successfully', async ({ page }) => {
    await page.goto('/contact');
    
    await page.fill('input[name="name"]', 'John Doe');
    await page.fill('input[name="email"]', 'john@example.com');
    await page.fill('textarea[name="message"]', 'Hello, this is a test message');
    
    await page.click('button[type="submit"]');
    
    await expect(page.locator('.success-message')).toBeVisible();
    await expect(page.locator('.success-message')).toContainText('Message sent');
  });
});

Testing Accessibility with Playwright

// tests/accessibility.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility', () => {
  test('home page passes axe', async ({ page }) => {
    await page.goto('/');
    
    const results = await new AxeBuilder({ page }).analyze();
    
    expect(results.violations).toEqual([]);
  });
  
  test('login page passes axe', async ({ page }) => {
    await page.goto('/login');
    
    const results = await new AxeBuilder({ page })
      .exclude('.third-party-widget') // Exclude elements you can't control
      .analyze();
    
    expect(results.violations).toEqual([]);
  });
  
  test('keyboard navigation works', async ({ page }) => {
    await page.goto('/');
    
    // Tab through focusable elements
    await page.keyboard.press('Tab');
    await expect(page.locator(':focus')).toHaveAttribute('href', '/');
    
    await page.keyboard.press('Tab');
    await expect(page.locator(':focus')).toHaveText('About');
    
    // Can activate with Enter
    await page.keyboard.press('Enter');
    await expect(page).toHaveURL('/about');
  });
});

📋 Package.json Scripts

{
  "scripts": {
    "test": "vitest",
    "test:unit": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:all": "npm run test:unit && npm run test:e2e"
  }
}

📋 Testing Checklist

Unit Tests

  • All components have tests
  • Props and variants tested
  • Event handlers tested
  • Edge cases covered
  • Async behavior tested

Integration Tests

  • Store interactions tested
  • Context providers tested
  • Form submissions tested
  • API calls mocked

E2E Tests

  • Critical user flows covered
  • Authentication tested
  • Form submissions tested
  • Error states tested
  • Accessibility automated