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.sveltefiles (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 testStart 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 testsQuick Reference
SSR Test Essentials
- ✅ Use
import { render } from 'svelte/server' - ✅ Test both
headandbodywhen relevant - ✅ Verify SEO-critical content renders
- ✅ Check accessibility attributes are present
- ✅ Ensure forms work without JavaScript
- ✅ Use
.ssr.test.tssuffix 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"');