Files
insta-recipe/docs/TESTING.md
Giancarmine Salucci 8545744bb1 fix(ssr): resolve EventSource SSR violations and implement best practices
- Fix EventSource is not defined error in queue dashboard
- Add browser guards for all EventSource usage
- Replace static constants (EventSource.OPEN/CLOSED) with numeric values
- Fix setInterval SSR violation in LLM health indicator
- Replace $effect anti-pattern with onMount in share page
- Add comprehensive SvelteKit SSR best practices documentation
- Add SSR audit and testing verification

All changes follow SvelteKit best practices and are verified against
official documentation. Production build succeeds with no SSR errors.

Closes: FixEventSourceSSR
See: docs/outcomes/FixEventSourceSSR.md
2025-12-22 03:00:29 +01:00

8.3 KiB

Vitest Mocking Guide for SvelteKit

This guide explains how to properly mock dependencies when testing SvelteKit applications with Vitest.

Understanding Mocking in SvelteKit Context

SvelteKit has a unique architecture where code can run on both server and client. This affects how we mock:

  1. Server-only modules ($lib/server/*, *.server.ts) - Only run on server
  2. Universal modules - Can run on both server and client
  3. Environment variables - Different modules for static vs dynamic access

Key Principles

  1. vi.mock() is hoisted - Always executed before imports
  2. Use factory functions - Return mocked implementations
  3. Mock before import - Mocks must be defined before the module is imported
  4. Clean up - Always restore/reset mocks in beforeEach or afterEach

Mocking Environment Variables ($env/dynamic/private)

Problem: Can't directly mock $env/dynamic/private because it's a SvelteKit magic module.

Solution: Create a config module that wraps env access, then mock the config.

Example: Queue Config Module

// src/lib/server/queue/config.ts
import { env } from '$env/dynamic/private';

export const queueConfig = {
  concurrency: parseInt(env.QUEUE_CONCURRENCY || '2', 10),
  maxRetries: parseInt(env.QUEUE_MAX_RETRIES || '3', 10),
  tandoor: {
    enabled: !!env.TANDOOR_TOKEN,
    token: env.TANDOOR_TOKEN || null
  }
};

Mocking the Config in Tests

import { vi, beforeEach, afterEach } from 'vitest';
import * as queueConfigModule from '$lib/server/queue/config';

// Mock the config module
vi.mock('$lib/server/queue/config', () => ({
  queueConfig: {
    concurrency: 2,
    maxRetries: 3,
    tandoor: { enabled: true, token: 'test-token' }
  }
}));

describe('QueueProcessor', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });
  
  afterEach(() => {
    vi.restoreAllMocks();
  });
});

Mocking External Service Modules

import { vi } from 'vitest';

// IMPORTANT: Mock BEFORE importing the module that uses it
vi.mock('$lib/server/extraction', () => ({
  extractTextAndThumbnail: vi.fn().mockResolvedValue({
    bodyText: 'Mock recipe text',
    thumbnail: 'https://mock.com/image.jpg'
  })
}));

// NOW import the module that depends on these
import { queueProcessor } from '$lib/server/queue/QueueProcessor';
import { extractTextAndThumbnail } from '$lib/server/extraction';

describe('QueueProcessor', () => {
  it('should use mocked services', async () => {
    const item = queueManager.enqueue('https://instagram.com/p/test');
    
    // Verify mock was called
    expect(extractTextAndThumbnail).toHaveBeenCalledWith(
      'https://instagram.com/p/test',
      expect.any(Function)
    );
  });
});

Mocking API Endpoints (SvelteKit RequestHandler)

When testing API endpoints, be aware that error responses must be properly awaited:

import { describe, it, expect } from 'vitest';
import { POST } from '../routes/api/queue/+server';

describe('POST /api/queue', () => {
  it('should reject invalid URLs', async () => {
    const request = new Request('http://localhost/api/queue', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url: 'invalid-url' })
    });
    
    const response = await POST({ request } as any);
    
    // ✅ CORRECT - Check status first
    expect(response.status).toBe(400);
    
    // ✅ CORRECT - Properly await error response
    const data = await response.json();
    expect(data.message).toContain('Invalid');
  });
});

Common Pitfall: Not Awaiting Error Responses

// ❌ WRONG - This will fail
it('should reject invalid input', async () => {
  const response = await endpoint({ request } as any);
  const data = response.json(); // Missing await!
  expect(data.message).toBe('Error'); // data is a Promise
});

// ✅ CORRECT
it('should reject invalid input', async () => {
  const response = await endpoint({ request } as any);
  expect(response.status).toBe(400);
  const data = await response.json(); // Properly awaited
  expect(data.message).toBe('Error');
});

Common Pitfalls and Solutions

Problem 1: Mock Not Working

// ❌ WRONG - Import before mock
import { queueProcessor } from './QueueProcessor';
vi.mock('./extraction');

// ✅ CORRECT - Mock before import
vi.mock('./extraction');
import { queueProcessor } from './QueueProcessor';

Problem 2: Mocks Not Resetting Between Tests

// ✅ SOLUTION - Always clean up
import { beforeEach, afterEach } from 'vitest';

beforeEach(() => {
  vi.clearAllMocks();  // Clear call history
});

afterEach(() => {
  vi.restoreAllMocks();  // Restore original implementations
});

Problem 3: TypeScript Errors with Mocked Functions

import { vi } from 'vitest';

// ✅ CORRECT - Type assertion
const mockFn = vi.fn<() => Promise<string>>();
mockFn.mockResolvedValue('test');

// OR use vi.mocked()
import { vi, type Mock } from 'vitest';
const mockFn = vi.fn() as Mock<() => Promise<string>>;

Testing Async Queue Processing

Solution 1: Wait for Processing

it('should process item', async () => {
  const item = queueManager.enqueue('https://instagram.com/p/test');
  
  // Wait for processing with timeout
  await vi.waitFor(
    () => {
      const updated = queueManager.get(item.id);
      expect(updated?.status).toBe('success');
    },
    { timeout: 5000, interval: 100 }
  );
});

Solution 2: Use Fake Timers

import { vi } from 'vitest';

beforeEach(() => {
  vi.useFakeTimers();
});

afterEach(() => {
  vi.useRealTimers();
});

it('should process after delay', async () => {
  queueManager.enqueue('https://test.com');
  
  // Fast-forward time
  await vi.advanceTimersByTimeAsync(1000);
  
  // Now check results
});

Best Practices for SvelteKit + Vitest

  1. Always mock before import - vi.mock() calls are hoisted but still need to be before your imports
  2. Use factory functions - Return new instances to avoid state leaking between tests
  3. Clean up thoroughly - Use beforeEach/afterEach to reset state
  4. Type your mocks - Use TypeScript generics for type-safe mocks
  5. Test isolation - Each test should be independent
  6. Mock at the right level - Mock external boundaries (HTTP, DB), not internal logic
  7. Use vi.waitFor() - For async operations instead of arbitrary setTimeout()
  8. Snapshot complex mocks - Use expect.any(Function) for callbacks
  9. Always await .json() on responses - Both success and error responses

Quick Reference: Mock Cheat Sheet

// Mock entire module
vi.mock('./module', () => ({ export: vi.fn() }));

// Mock with factory
vi.mock('./module', () => {
  return { dynamicExport: () => 'value' };
});

// Spy on existing export
vi.spyOn(module, 'export').mockReturnValue('value');

// Mock return value
mockFn.mockReturnValue('sync value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('async error'));

// Mock implementation
mockFn.mockImplementation((arg) => arg * 2);
mockFn.mockImplementationOnce((arg) => arg * 3);

// Check calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('arg');

// Reset/restore
vi.clearAllMocks();      // Clear call history
vi.resetAllMocks();      // + Reset implementations
vi.restoreAllMocks();    // + Restore original implementations

// Environment variables
vi.stubEnv('VAR_NAME', 'value');
vi.unstubAllEnvs();

// Timers
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
await vi.advanceTimersByTimeAsync(1000);
vi.useRealTimers();

// Async helpers
await vi.waitFor(() => expect(condition).toBe(true));
await vi.waitUntil(() => condition === true);

Examples from This Project

See the following test files for real-world examples:

  • src/tests/queue-manager.spec.ts - Mocking external services
  • src/tests/queue-processor.spec.ts - Mocking config module and services
  • src/tests/queue-api.spec.ts - Testing API endpoints with proper async handling

Additional Resources