SSR Testing Patterns

Overview

SSR (Server-Side Rendering) tests ensure server-rendered HTML matches client expectations and prevent hydration mismatches. This guide covers when to add SSR tests and the essential patterns for testing server-rendered components.

For component testing patterns, see Testing Patterns. For best practices, see Best Practices.

Optional Extension to the CLI Scaffold

The official CLI creates client and server Vitest projects. SSR tests can run in its Node server project. Sveltest separates them so pnpm test:ssr can run rendering coverage independently.

To do the same, add this entry to test.projects:

1{
2	extends: './vite.config.ts',
3	test: {
4		name: 'ssr',
5		environment: 'node',
6		include: ['src/**/*.ssr.{test,spec}.{js,ts}'],
7	},
8}

Also add src/**/*.ssr.{test,spec}.{js,ts} to the server project’s exclude, alongside its component-test exclusion. This prevents running SSR tests twice. Keep expect: { requireAssertions: true } at the root of test, as in the official scaffold.

When to Add SSR Tests

Always Add SSR Tests For:

  • Form components - Inputs, selects, textareas (progressive enhancement critical)
  • Navigation components - Links, menus, breadcrumbs (SEO + accessibility)
  • Content components - Cards, articles, headers (SEO critical)
  • Layout components - Page shells, grids (hydration mismatch prone)

Usually Add SSR Tests For:

  • Components with complex CSS logic - Conditional classes, variants
  • Components with ARIA attributes - Screen reader compatibility
  • Components that render different content server vs client
  • Components used in +page.svelte files (always SSR’d)

Rarely Need SSR Tests For:

  • Pure interaction components - Modals, dropdowns, tooltips
  • Client-only components - Charts, maps, rich editors
  • Simple presentational components - Icons, badges, dividers

Red Flags That Require SSR Tests:

  • Hydration mismatches in browser console
  • Different appearance on first load vs after hydration
  • SEO issues with missing content
  • Accessibility tools can’t find elements
  • Form doesn’t work without JavaScript

Quick Decision Framework:

1Does it render different HTML server vs client? → SSR test
2Is it SEO critical? → SSR test
3Does it need to work without JS? → SSR test
4Is it just interactive behavior? → Skip SSR test

Start with browser tests only, add SSR tests when you hit problems or have specific SSR requirements.

Basic SSR Pattern

1import { render } from 'svelte/server';
2import { describe, expect, test } from 'vitest';
3
4describe('Component SSR', () => {
5	it('should render without errors', () => {
6		expect(() => {
7			render(ComponentName);
8		}).not.toThrow();
9	});
10
11	it('should render essential content for SEO', () => {
12		const { body } = render(ComponentName, {
13			props: { title: 'Page Title', description: 'Page description' },
14		});
15
16		expect(body).toContain('<h1>Page Title</h1>');
17		expect(body).toContain('Page description');
18		expect(body).toContain('href="/important-link"');
19	});
20
21	it('should render meta information', () => {
22		const { head } = render(ComponentName, {
23			props: { title: 'Page Title' },
24		});
25
26		expect(head).toContain('<title>Page Title</title>');
27		expect(head).toContain('meta name="description"');
28	});
29});

Passing Children with createRawSnippet

Svelte 5 components use snippets for children. In SSR tests, use createRawSnippet() to pass children content:

1import { render } from 'svelte/server';
2import { createRawSnippet } from 'svelte';
3import Button from './button.svelte';
4
5describe('Button SSR', () => {
6	it('should render with children content', () => {
7		const { body } = render(Button, {
8			props: {
9				children: createRawSnippet(() => ({
10					render: () => 'Click me',
11				})),
12			},
13		});
14
15		expect(body).toContain('Click me');
16		expect(body).toContain('<button');
17	});
18
19	it('should render with HTML children', () => {
20		const { body } = render(Button, {
21			props: {
22				children: createRawSnippet(() => ({
23					render: () => '<span class="icon">★</span> Star',
24				})),
25			},
26		});
27
28		expect(body).toContain('★');
29		expect(body).toContain('Star');
30	});
31});

This pattern is essential for any component that accepts children via {@render children?.()} in its template.

Layout SSR Pattern

1describe('Layout SSR', () => {
2	it('should render navigation structure', () => {
3		const { body } = render(Layout);
4
5		expect(body).toContain('<nav');
6		expect(body).toContain('aria-label="Main navigation"');
7		expect(body).toContain('href="/docs"');
8		expect(body).toContain('href="/examples"');
9	});
10
11	it('should include accessibility features', () => {
12		const { body } = render(Layout);
13
14		expect(body).toContain('role="main"');
15		expect(body).toContain('aria-label');
16		expect(body).toContain('skip-to-content');
17	});
18
19	it('should render footer information', () => {
20		const { body } = render(Layout);
21
22		expect(body).toContain('<footer');
23		expect(body).toContain('© 2024');
24		expect(body).toContain('Privacy Policy');
25	});
26});

Head and Body Testing

SSR tests use svelte/server’s render function which returns both head and body content:

1import { render } from 'svelte/server';
2
3describe('SEO Component SSR', () => {
4	it('should render head content for SEO', () => {
5		const { head, body } = render(SEOComponent, {
6			props: {
7				title: 'My Page',
8				description: 'A description',
9				og_image: '/images/og.png',
10			},
11		});
12
13		// Test head content (meta tags, title, etc.)
14		expect(head).toContain('<title>My Page</title>');
15		expect(head).toContain('meta name="description"');
16		expect(head).toContain('og:image');
17
18		// Test body content
19		expect(body).toContain('<main');
20	});
21
22	it('should render structured data', () => {
23		const { head } = render(ArticlePage, {
24			props: { article: mock_article },
25		});
26
27		expect(head).toContain('application/ld+json');
28		expect(head).toContain('"@type":"Article"');
29	});
30});

Form SSR Pattern

Forms require SSR tests to ensure progressive enhancement works:

1describe('ContactForm SSR', () => {
2	it('should render form elements for no-JS fallback', () => {
3		const { body } = render(ContactForm);
4
5		// Form should have action for no-JS submission
6		expect(body).toContain('action="/api/contact"');
7		expect(body).toContain('method="POST"');
8
9		// All inputs should be present
10		expect(body).toContain('name="email"');
11		expect(body).toContain('name="message"');
12		expect(body).toContain('type="submit"');
13	});
14
15	it('should render labels for accessibility', () => {
16		const { body } = render(ContactForm);
17
18		expect(body).toContain('<label');
19		expect(body).toContain('for="email"');
20		expect(body).toContain('for="message"');
21	});
22});

SSR Test File Organization

SSR tests are co-located with components using .ssr.test.ts suffix:

1src/lib/components/
2├── button.svelte
3├── button.svelte.test.ts      # Client-side browser tests
4└── button.ssr.test.ts         # Server-side rendering tests
5
6src/routes/
7├── +page.svelte
8├── page.svelte.test.ts        # Component tests
9└── page.ssr.test.ts           # SSR tests

Quick Reference

SSR Test Essentials

  • ✅ Use import { render } from 'svelte/server'
  • ✅ Test both head and body when relevant
  • ✅ Verify SEO-critical content renders
  • ✅ Check accessibility attributes are present
  • ✅ Ensure forms work without JavaScript
  • ✅ Use .ssr.test.ts suffix for SSR test files

Common SSR Checks

1// SEO content
2expect(body).toContain('<h1>');
3expect(head).toContain('<title>');
4expect(head).toContain('meta name="description"');
5
6// Accessibility
7expect(body).toContain('aria-label');
8expect(body).toContain('role="main"');
9
10// Progressive enhancement
11expect(body).toContain('action="/api/');
12expect(body).toContain('method="POST"');