]*>\+<\/button>/);
88 | });
89 |
90 | test('should render with responsive layout classes', () => {
91 | const { body } = render(Calculator);
92 |
93 | // Test responsive and layout classes
94 | expect(body).toContain(
95 | 'bg-base-200/50 border-base-300/50 rounded-xl border p-6',
96 | );
97 | expect(body).toContain('col-span-2'); // Zero button spans 2 columns
98 | });
99 |
100 | test('should not contain client-side JavaScript in SSR output', () => {
101 | const { body } = render(Calculator);
102 |
103 | // SSR should not contain onclick handlers in the HTML
104 | // The onclick handlers are added on the client side
105 | expect(body).not.toContain('onclick');
106 | expect(body).not.toContain('calculator_state');
107 | });
108 |
109 | test('should render calculator in a clean state', () => {
110 | const { body } = render(Calculator);
111 |
112 | // Should show initial state
113 | expect(body).toContain('0'); // Default display value
114 |
115 | // Should not contain any calculation results or intermediate states
116 | expect(body).not.toContain('Error');
117 | expect(body).not.toContain('NaN');
118 | expect(body).not.toContain('Infinity');
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/src/lib/components/calculator.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
9 |
12 | {calculator_state.current_value}
13 |
14 |
15 |
16 | calculator_state.clear()}
19 | >
20 | C
21 |
22 | ±
23 | %
24 | calculator_state.input_operation('/')}
27 | >
28 | ÷
29 |
30 |
31 | calculator_state.input_digit('7')}
34 | >
35 | 7
36 |
37 | calculator_state.input_digit('8')}
40 | >
41 | 8
42 |
43 | calculator_state.input_digit('9')}
46 | >
47 | 9
48 |
49 | calculator_state.input_operation('*')}
52 | >
53 | ×
54 |
55 |
56 | calculator_state.input_digit('4')}
59 | >
60 | 4
61 |
62 | calculator_state.input_digit('5')}
65 | >
66 | 5
67 |
68 | calculator_state.input_digit('6')}
71 | >
72 | 6
73 |
74 | calculator_state.input_operation('-')}
77 | >
78 | −
79 |
80 |
81 | calculator_state.input_digit('1')}
84 | >
85 | 1
86 |
87 | calculator_state.input_digit('2')}
90 | >
91 | 2
92 |
93 | calculator_state.input_digit('3')}
96 | >
97 | 3
98 |
99 | calculator_state.input_operation('+')}
102 | >
103 | +
104 |
105 |
106 | calculator_state.input_digit('0')}
109 | >
110 | 0
111 |
112 | calculator_state.input_digit('.')}
115 | >
116 | .
117 |
118 | calculator_state.perform_calculation()}
121 | >
122 | =
123 |
124 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/src/lib/components/card.svelte:
--------------------------------------------------------------------------------
1 |
107 |
108 |
109 |
120 | {#if image_src}
121 |
127 | {/if}
128 |
129 | {#if title}
130 |
131 | {title}
132 |
133 | {/if}
134 |
135 | {#if subtitle}
136 |
137 | {subtitle}
138 |
139 | {/if}
140 |
141 | {#if content_text}
142 |
143 |
{content_text}
144 |
145 | {/if}
146 |
147 | {#if footer_text}
148 |
151 | {/if}
152 |
153 |
--------------------------------------------------------------------------------
/src/lib/components/code-block.ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { render } from 'svelte/server';
2 | import { describe, expect, test } from 'vitest';
3 | import CodeBlock from './code-block.svelte';
4 |
5 | describe('CodeBlock SSR', () => {
6 | test('should render without errors', () => {
7 | expect(() => {
8 | render(CodeBlock, {
9 | props: {
10 | code: 'const hello = "world";',
11 | },
12 | });
13 | }).not.toThrow();
14 | });
15 |
16 | test('should render fallback code content for SEO', () => {
17 | const { body } = render(CodeBlock, {
18 | props: {
19 | code: 'console.log("Hello, World!");',
20 | lang: 'javascript',
21 | },
22 | });
23 |
24 | // Should render the actual code content in fallback format
25 | expect(body).toContain('console.log("Hello, World!");');
26 | // Check for fallback class (may have scoped CSS suffix)
27 | expect(body).toMatch(/class="code-fallback[^"]*"/);
28 | // Should contain pre and code elements
29 | expect(body).toContain(' {
34 | const { body } = render(CodeBlock, {
35 | props: {
36 | code: 'function test() { return true; }',
37 | lang: 'typescript',
38 | },
39 | });
40 |
41 | // Should render the actual code content regardless of language
42 | expect(body).toContain('function test() { return true; }');
43 | expect(body).toMatch(/class="code-fallback[^"]*"/);
44 | });
45 |
46 | test('should handle empty code in SSR', () => {
47 | const { body } = render(CodeBlock, {
48 | props: {
49 | code: '',
50 | },
51 | });
52 |
53 | // Should still render fallback structure even with empty code
54 | expect(body).toMatch(/class="code-fallback[^"]*"/);
55 | expect(body).toContain(' {
60 | const { body } = render(CodeBlock, {
61 | props: {
62 | code: 'const test = true;',
63 | theme: 'night-owl',
64 | },
65 | });
66 |
67 | // Should render fallback with any theme (theme doesn't affect SSR fallback)
68 | expect(body).toContain('const test = true;');
69 | expect(body).toMatch(/class="code-fallback[^"]*"/);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/src/lib/components/code-block.svelte:
--------------------------------------------------------------------------------
1 |
56 |
57 | {#if is_loading && browser}
58 | Loading...
59 | {:else if is_enhanced && highlighted_code}
60 | {@html highlighted_code}
61 | {:else}
62 |
63 | {code}
64 | {/if}
65 |
66 |
117 |
--------------------------------------------------------------------------------
/src/lib/components/doc-card.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 |
31 |
32 |
35 |
36 |
37 | {#if index !== undefined}
38 |
41 | {index + 1}
42 |
43 | {:else}
44 |
48 | {/if}
49 |
50 |
51 |
54 | {title}
55 |
56 |
57 |
58 | {description}
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/lib/components/docs-search.svelte.test.ts:
--------------------------------------------------------------------------------
1 | import { page } from '@vitest/browser/context';
2 | import { describe, expect, test, vi } from 'vitest';
3 | import { render } from 'vitest-browser-svelte';
4 | import DocsSearch from './docs-search.svelte';
5 |
6 | // Mock fetch for API calls - use vi.stubGlobal for better CI compatibility
7 | const mock_fetch = vi.fn();
8 | vi.stubGlobal('fetch', mock_fetch);
9 |
10 | describe('DocsSearch', () => {
11 | describe('Initial Rendering', () => {
12 | test('should render search input', async () => {
13 | render(DocsSearch);
14 |
15 | await expect
16 | .element(page.getByLabelText('Search Documentation'))
17 | .toBeInTheDocument();
18 |
19 | await expect
20 | .element(page.getByTestId('docs-search-input'))
21 | .toBeInTheDocument();
22 | });
23 |
24 | test('should show search placeholder', async () => {
25 | render(DocsSearch);
26 |
27 | const input = page.getByTestId('docs-search-input');
28 | await expect
29 | .element(input)
30 | .toHaveAttribute(
31 | 'placeholder',
32 | 'Search topics, examples, patterns...',
33 | );
34 | });
35 | });
36 |
37 | describe('Search Functionality', () => {
38 | test.skip('should show loading spinner when searching', async () => {
39 | // TODO: Loading spinner doesn't have test ID and page.locator() not available
40 | // Skip for now - would need to add data-testid to loading spinner in component
41 | });
42 |
43 | test.skip('should show clear button when text is entered', async () => {
44 | // TODO: This component uses native search input clear functionality
45 | // Skip this test as there's no custom clear button
46 | });
47 |
48 | test.skip('should clear search when clear button is clicked', async () => {
49 | // TODO: This component uses native search input clear functionality
50 | // Skip this test as there's no custom clear button
51 | });
52 | });
53 |
54 | describe('Search Results', () => {
55 | test.skip('should show search results after typing', async () => {
56 | // TODO: This test requires proper async handling with API
57 | // Skip for now due to timing issues with debounced search + API calls
58 | });
59 |
60 | test.skip('should show no results message when no matches found', async () => {
61 | // TODO: Test no results state with API
62 | });
63 | });
64 |
65 | describe('Keyboard Shortcuts', () => {
66 | test('should show keyboard shortcut hint', async () => {
67 | render(DocsSearch);
68 |
69 | await expect
70 | .element(page.getByText('Ctrl'))
71 | .toBeInTheDocument();
72 |
73 | await expect.element(page.getByText('k')).toBeInTheDocument();
74 | });
75 |
76 | test.skip('should focus input on Ctrl+K', async () => {
77 | // TODO: Test keyboard shortcuts - requires proper event simulation
78 | // Skip for now due to complexity of testing global keyboard events
79 | });
80 |
81 | test.skip('should clear search on Escape', async () => {
82 | // TODO: Test escape key functionality
83 | });
84 | });
85 |
86 | describe('Accessibility', () => {
87 | test('should have proper labels and ARIA attributes', async () => {
88 | render(DocsSearch);
89 |
90 | const input = page.getByTestId('docs-search-input');
91 | await expect
92 | .element(input)
93 | .toHaveAttribute('id', 'docs-search');
94 |
95 | const label = page.getByText('Search Documentation');
96 | await expect.element(label).toBeInTheDocument();
97 | });
98 |
99 | test.skip('should have clear button with proper aria-label', async () => {
100 | // TODO: This component uses native search input clear functionality
101 | // Skip this test as there's no custom clear button
102 | });
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/src/lib/components/docs-toc.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
24 |
25 |
26 |
27 | Documentation
28 |
29 |
30 |
31 |
50 |
51 |
52 |
53 |
54 |
65 |
--------------------------------------------------------------------------------
/src/lib/components/feature-card.svelte:
--------------------------------------------------------------------------------
1 |
46 |
47 |
93 |
--------------------------------------------------------------------------------
/src/lib/components/github-status-pills.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | {#if github_status.data}
6 |
7 |
8 |
11 |
19 |
22 | Unit Tests
23 |
24 |
25 |
26 |
27 |
30 |
38 |
41 | E2E Tests
42 |
43 |
44 |
45 | {:else if github_status.loading}
46 |
47 |
48 | Loading status...
50 |
51 | {/if}
52 |
--------------------------------------------------------------------------------
/src/lib/components/input.ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { render } from 'svelte/server';
2 | import { describe, expect, test } from 'vitest';
3 | import Input from './input.svelte';
4 |
5 | describe('Input Component SSR', () => {
6 | describe('Server-Side Rendering', () => {
7 | test('should render without errors', () => {
8 | expect(() => {
9 | render(Input, {
10 | props: {
11 | label: 'Test Input',
12 | },
13 | });
14 | }).not.toThrow();
15 | });
16 |
17 | test('should render essential form elements for SEO', () => {
18 | const { body } = render(Input, {
19 | props: {
20 | label: 'Email Address',
21 | type: 'email',
22 | name: 'email',
23 | required: true,
24 | },
25 | });
26 |
27 | // Essential HTML structure
28 | expect(body).toContain(' {
37 | const { body } = render(Input, {
38 | props: {
39 | label: 'Email',
40 | error: 'This field is required',
41 | },
42 | });
43 |
44 | expect(body).toContain('This field is required');
45 | expect(body).toContain('aria-invalid="true"');
46 | expect(body).toContain('role="alert"');
47 | });
48 |
49 | test('should render without label when not provided', () => {
50 | const { body } = render(Input, {
51 | props: {},
52 | });
53 |
54 | expect(body).toContain(' {
59 | const { body } = render(Input, {
60 | props: {
61 | label: 'Username',
62 | value: 'john_doe',
63 | },
64 | });
65 |
66 | expect(body).toContain('value="john_doe"');
67 | });
68 |
69 | test('should handle complex prop combinations', () => {
70 | expect(() => {
71 | render(Input, {
72 | props: {
73 | type: 'email',
74 | label: 'Email',
75 | placeholder: 'Enter email',
76 | error: 'Invalid email',
77 | required: true,
78 | name: 'user_email',
79 | value: 'test@example.com',
80 | },
81 | });
82 | }).not.toThrow();
83 | });
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/src/lib/components/input.svelte:
--------------------------------------------------------------------------------
1 |
87 |
88 | {#if label}
89 |
90 |
91 | {label}
92 | {#if required}
93 | *
98 | {/if}
99 |
100 |
101 | {/if}
102 |
103 |
104 | {#if prefix}
105 |
106 | {@render prefix()}
107 |
108 | {/if}
109 |
110 |
135 |
136 | {#if suffix}
137 |
138 | {@render suffix()}
139 |
140 | {/if}
141 |
142 |
143 | {#if has_error}
144 |
145 |
152 | {error}
153 |
154 |
155 | {/if}
156 |
--------------------------------------------------------------------------------
/src/lib/components/logo.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
27 |
28 |
29 |
30 |
36 |
37 |
38 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
60 |
61 |
67 |
68 |
69 |
70 |
74 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/src/lib/data/topics.ts:
--------------------------------------------------------------------------------
1 | export interface Topic {
2 | slug: string;
3 | title: string;
4 | description: string;
5 | }
6 |
7 | export const topics: Topic[] = [
8 | {
9 | slug: 'getting-started',
10 | title: 'Getting Started',
11 | description: 'Setup, installation, and your first test',
12 | },
13 | {
14 | slug: 'testing-patterns',
15 | title: 'Testing Patterns',
16 | description: 'Component, SSR, and server testing patterns',
17 | },
18 | {
19 | slug: 'e2e-testing',
20 | title: 'E2E Testing',
21 | description:
22 | 'End-to-end testing patterns and integration validation',
23 | },
24 | {
25 | slug: 'api-reference',
26 | title: 'API Reference',
27 | description: 'Complete testing utilities and helper functions',
28 | },
29 | {
30 | slug: 'migration-guide',
31 | title: 'Migration Guide',
32 | description: 'Migrating from @testing-library/svelte',
33 | },
34 | {
35 | slug: 'best-practices',
36 | title: 'Best Practices',
37 | description: 'Advanced patterns and optimization techniques',
38 | },
39 | {
40 | slug: 'ci-cd',
41 | title: 'CI/CD',
42 | description: 'Production-ready testing pipelines and automation',
43 | },
44 | {
45 | slug: 'troubleshooting',
46 | title: 'Troubleshooting',
47 | description: 'Common issues and solutions',
48 | },
49 | {
50 | slug: 'about',
51 | title: 'About',
52 | description: 'About this project and its goals',
53 | },
54 | ];
55 |
--------------------------------------------------------------------------------
/src/lib/icons/arrow.ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { render } from 'svelte/server';
2 | import { describe, expect, test } from 'vitest';
3 | import Arrow from './arrow.svelte';
4 |
5 | describe('Arrow SSR', () => {
6 | test('should render without errors', () => {
7 | expect(() => {
8 | render(Arrow);
9 | }).not.toThrow();
10 | });
11 |
12 | test('should render with all direction variants', () => {
13 | const directions = ['up', 'down', 'left', 'right'] as const;
14 |
15 | directions.forEach((direction) => {
16 | expect(() => {
17 | render(Arrow, { props: { direction } });
18 | }).not.toThrow();
19 | });
20 | });
21 |
22 | test('should render essential SVG structure for all browsers', () => {
23 | const { body } = render(Arrow);
24 |
25 | // ✅ Test essential SVG structure for SEO and accessibility
26 | expect(body).toContain(' {
41 | const test_cases = [
42 | { direction: 'up', expected_rotation: '270deg' },
43 | { direction: 'right', expected_rotation: '0deg' },
44 | { direction: 'down', expected_rotation: '90deg' },
45 | { direction: 'left', expected_rotation: '180deg' },
46 | ] as const;
47 |
48 | test_cases.forEach(({ direction, expected_rotation }) => {
49 | const { body } = render(Arrow, { props: { direction } });
50 | expect(body).toContain(
51 | `transform: rotate(${expected_rotation})`,
52 | );
53 | });
54 | });
55 |
56 | test('should render with custom dimensions', () => {
57 | const { body } = render(Arrow, {
58 | props: { height: '32px', width: '32px' },
59 | });
60 |
61 | expect(body).toContain('height="32px"');
62 | expect(body).toContain('width="32px"');
63 | });
64 |
65 | test('should render with custom class names', () => {
66 | const { body } = render(Arrow, {
67 | props: { class_names: 'custom-arrow-class' },
68 | });
69 |
70 | expect(body).toContain('class="custom-arrow-class"');
71 | });
72 |
73 | test('should render default values when props are undefined', () => {
74 | const { body } = render(Arrow, {
75 | props: {
76 | direction: undefined,
77 | height: undefined,
78 | width: undefined,
79 | class_names: undefined,
80 | },
81 | });
82 |
83 | // Should use default values
84 | expect(body).toContain('height="24px"');
85 | expect(body).toContain('width="24px"');
86 | expect(body).toContain('transform: rotate(0deg)'); // default right direction
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/src/lib/icons/arrow.svelte:
--------------------------------------------------------------------------------
1 |
47 |
48 |
61 |
66 |
67 |
--------------------------------------------------------------------------------
/src/lib/icons/bar-chart.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/beaker.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/book-open.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/calculator.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/check-circle.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/check.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/chevron.svelte:
--------------------------------------------------------------------------------
1 |
45 |
46 |
59 |
64 |
65 |
--------------------------------------------------------------------------------
/src/lib/icons/circle-dot.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
27 |
28 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/icons/clipboard.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/clock.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/code.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/cursor.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
40 |
--------------------------------------------------------------------------------
/src/lib/icons/document.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/external-link.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
27 |
32 |
33 |
--------------------------------------------------------------------------------
/src/lib/icons/eye-off.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/eye.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
39 |
40 |
--------------------------------------------------------------------------------
/src/lib/icons/filter.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/github-fork.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
27 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/icons/github.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
26 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/icons/heart.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
35 |
40 |
41 |
--------------------------------------------------------------------------------
/src/lib/icons/home.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Arrow } from './arrow.svelte';
2 | export { default as BarChart } from './bar-chart.svelte';
3 | export { default as Beaker } from './beaker.svelte';
4 | export { default as BookOpen } from './book-open.svelte';
5 | export { default as Calculator } from './calculator.svelte';
6 | export { default as CheckCircle } from './check-circle.svelte';
7 | export { default as Check } from './check.svelte';
8 | export { default as Chevron } from './chevron.svelte';
9 | export { default as CircleDot } from './circle-dot.svelte';
10 | export { default as Clipboard } from './clipboard.svelte';
11 | export { default as Clock } from './clock.svelte';
12 | export { default as Code } from './code.svelte';
13 | export { default as Cursor } from './cursor.svelte';
14 | export { default as Document } from './document.svelte';
15 | export { default as ExternalLink } from './external-link.svelte';
16 | export { default as EyeOff } from './eye-off.svelte';
17 | export { default as Eye } from './eye.svelte';
18 | export { default as Filter } from './filter.svelte';
19 | export { default as GitHubFork } from './github-fork.svelte';
20 | export { default as GitHub } from './github.svelte';
21 | export { default as Heart } from './heart.svelte';
22 | export { default as Home } from './home.svelte';
23 | export { default as LightningBolt } from './lightning-bolt.svelte';
24 | export { default as Menu } from './menu.svelte';
25 | export { default as MoreVertical } from './more-vertical.svelte';
26 | export { default as Plus } from './plus.svelte';
27 | export { default as Robot } from './robot.svelte';
28 | export { default as Search } from './search.svelte';
29 | export { default as Server } from './server.svelte';
30 | export { default as Settings } from './settings.svelte';
31 | export { default as Trash } from './trash.svelte';
32 | export { default as User } from './user.svelte';
33 | export { default as Windsurf } from './windsurf.svelte';
34 | export { default as XCircle } from './x-circle.svelte';
35 | export { default as X } from './x.svelte';
36 |
--------------------------------------------------------------------------------
/src/lib/icons/lightning-bolt.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/menu.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
35 |
36 |
--------------------------------------------------------------------------------
/src/lib/icons/more-vertical.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/plus.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/robot.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
27 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/icons/search.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/server.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/settings.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
39 |
40 |
--------------------------------------------------------------------------------
/src/lib/icons/trash.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/user.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/windsurf.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
31 |
--------------------------------------------------------------------------------
/src/lib/icons/x-circle.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/icons/x.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | // place files you want to import through the `$lib` alias in this folder.
2 | export { default as Button } from './components/button.svelte';
3 | export { default as Calculator } from './components/calculator.svelte';
4 | export { default as Card } from './components/card.svelte';
5 | export { default as CodeBlock } from './components/code-block.svelte';
6 | export { default as DocsSearch } from './components/docs-search.svelte';
7 | export { default as Input } from './components/input.svelte';
8 | export { default as LoginForm } from './components/login-form.svelte';
9 | export { default as Modal } from './components/modal.svelte';
10 | export { default as SiteSearch } from './components/site-search.svelte';
11 | export { default as TodoManager } from './components/todo-manager.svelte';
12 |
13 | // Utility exports
14 | export * from './state/form-state.svelte.ts';
15 | export * from './utils/validation.ts';
16 |
--------------------------------------------------------------------------------
/src/lib/server/llms.ts:
--------------------------------------------------------------------------------
1 | import { topics } from '$lib/data/topics';
2 | import { readFile } from 'node:fs/promises';
3 | import { join } from 'node:path';
4 |
5 | // Import all markdown files using Vite's ?raw imports
6 | import about from '../../copy/about.md?raw';
7 | import api_reference from '../../copy/api-reference.md?raw';
8 | import best_practices from '../../copy/best-practices.md?raw';
9 | import ci_cd from '../../copy/ci-cd.md?raw';
10 | import e2e_testing from '../../copy/e2e-testing.md?raw';
11 | import getting_started from '../../copy/getting-started.md?raw';
12 | import migration_guide from '../../copy/migration-guide.md?raw';
13 | import testing_patterns from '../../copy/testing-patterns.md?raw';
14 | import troubleshooting from '../../copy/troubleshooting.md?raw';
15 |
16 | export { topics };
17 |
18 | // Content map using the imported markdown
19 | const content_map: Record = {
20 | 'getting-started': getting_started,
21 | 'testing-patterns': testing_patterns,
22 | 'e2e-testing': e2e_testing,
23 | 'api-reference': api_reference,
24 | 'migration-guide': migration_guide,
25 | 'best-practices': best_practices,
26 | 'ci-cd': ci_cd,
27 | troubleshooting: troubleshooting,
28 | about: about,
29 | };
30 |
31 | // Function to load full content from preloaded markdown (no async needed!)
32 | export function load_full_content(): string {
33 | let content = '# Sveltest Testing Documentation\n\n';
34 | content +=
35 | '> 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.\n\n';
36 |
37 | for (const topic of topics) {
38 | const md_content = content_map[topic.slug];
39 | if (md_content) {
40 | content += `\n# ${topic.title}\n\n`;
41 | content += md_content;
42 | content += '\n';
43 | } else {
44 | console.warn(`No content found for topic: ${topic.slug}`);
45 | }
46 | }
47 |
48 | return content;
49 | }
50 |
51 | // Load prompts from markdown files (for server-side use)
52 | async function loadPrompts(): Promise> {
53 | const variants = [
54 | 'llms',
55 | 'llms-medium',
56 | 'llms-small',
57 | 'llms-api',
58 | 'llms-examples',
59 | 'llms-ctx',
60 | ];
61 | const prompts: Record = {};
62 |
63 | for (const variant of variants) {
64 | try {
65 | const promptPath = join(
66 | process.cwd(),
67 | 'prompts',
68 | `${variant}.md`,
69 | );
70 | prompts[variant] = await readFile(promptPath, 'utf-8');
71 | } catch (error) {
72 | console.warn(`Could not load prompt for ${variant}:`, error);
73 | }
74 | }
75 |
76 | return prompts;
77 | }
78 |
79 | // Export the function for runtime loading
80 | export async function getVariantPrompts() {
81 | return await loadPrompts();
82 | }
83 |
--------------------------------------------------------------------------------
/src/lib/server/search-index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest';
2 | import {
3 | generate_search_index,
4 | search_full_text,
5 | } from './search-index';
6 |
7 | describe('Search Index', () => {
8 | test('should generate search index with full content', async () => {
9 | const index = await generate_search_index();
10 |
11 | expect(index.items.length).toBeGreaterThan(0);
12 | expect(index.total_items).toBe(index.items.length);
13 | expect(index.generated_at).toBeDefined();
14 |
15 | // Should have documentation topics
16 | const docs = index.items.filter((item) => item.type === 'topic');
17 | expect(docs.length).toBeGreaterThan(5);
18 |
19 | // Should have code examples
20 | const examples = index.items.filter(
21 | (item) => item.type === 'example',
22 | );
23 | expect(examples.length).toBeGreaterThan(10);
24 |
25 | // Items should have full content
26 | const api_reference = index.items.find(
27 | (item) => item.id === 'topic-api-reference',
28 | );
29 | expect(api_reference).toBeDefined();
30 | expect(api_reference!.content.length).toBeGreaterThan(1000);
31 | expect(api_reference!.keywords).toContain('mock');
32 | });
33 |
34 | test('should find "mock" in documentation', async () => {
35 | const index = await generate_search_index();
36 | const results = search_full_text('mock', index);
37 |
38 | expect(results.length).toBeGreaterThan(0);
39 |
40 | // Should find API Reference and Best Practices (both contain extensive mock content)
41 | const api_reference = results.find(
42 | (item) => item.id === 'topic-api-reference',
43 | );
44 | const best_practices = results.find(
45 | (item) => item.id === 'topic-best-practices',
46 | );
47 |
48 | expect(api_reference).toBeDefined();
49 | expect(best_practices).toBeDefined();
50 |
51 | // Results should be scored (higher scores first)
52 | expect(results[0].score).toBeGreaterThan(0);
53 | if (results.length > 1) {
54 | expect(results[0].score).toBeGreaterThanOrEqual(
55 | results[1].score,
56 | );
57 | }
58 | });
59 |
60 | test('should find "vi.fn" in documentation', async () => {
61 | const index = await generate_search_index();
62 | const results = search_full_text('vi.fn', index);
63 |
64 | expect(results.length).toBeGreaterThan(0);
65 |
66 | // Should find content with vi.fn
67 | const has_vi_fn = results.some(
68 | (item) =>
69 | item.content.toLowerCase().includes('vi.fn') ||
70 | item.keywords.includes('vi.fn'),
71 | );
72 | expect(has_vi_fn).toBe(true);
73 | });
74 |
75 | test('should filter results by category', async () => {
76 | const index = await generate_search_index();
77 |
78 | // Test docs filter
79 | const docs_results = search_full_text('test', index, 'docs');
80 | docs_results.forEach((result) => {
81 | expect(
82 | result.type === 'topic' ||
83 | result.category === 'Documentation' ||
84 | result.category === 'Quick Start',
85 | ).toBe(true);
86 | });
87 |
88 | // Test examples filter
89 | const examples_results = search_full_text(
90 | 'test',
91 | index,
92 | 'examples',
93 | );
94 | examples_results.forEach((result) => {
95 | expect(result.type).toBe('example');
96 | expect([
97 | 'Components',
98 | 'Documentation',
99 | 'Quick Start',
100 | ]).not.toContain(result.category);
101 | });
102 | });
103 |
104 | test('should handle empty queries gracefully', async () => {
105 | const index = await generate_search_index();
106 |
107 | const empty_results = search_full_text('', index);
108 | expect(empty_results).toEqual([]);
109 |
110 | const whitespace_results = search_full_text(' ', index);
111 | expect(whitespace_results).toEqual([]);
112 | });
113 |
114 | test('should extract relevant keywords', async () => {
115 | const index = await generate_search_index();
116 |
117 | // Find an item with mock-related content
118 | const mock_item = index.items.find(
119 | (item) =>
120 | item.keywords.includes('mock') ||
121 | item.keywords.includes('vi.fn'),
122 | );
123 |
124 | expect(mock_item).toBeDefined();
125 | expect(mock_item!.keywords.length).toBeGreaterThan(0);
126 |
127 | // Should extract testing-related keywords
128 | const testing_keywords = [
129 | 'mock',
130 | 'test',
131 | 'expect',
132 | 'vi.fn',
133 | 'render',
134 | ];
135 | const has_testing_keywords = testing_keywords.some((keyword) =>
136 | mock_item!.keywords.includes(keyword),
137 | );
138 | expect(has_testing_keywords).toBe(true);
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/src/lib/state/calculator.svelte.ts:
--------------------------------------------------------------------------------
1 | class CalculatorState {
2 | private _current_value = $state('0');
3 | private _previous_value = $state('');
4 | private _operation = $state('');
5 | private _waiting_for_operand = $state(false);
6 |
7 | get current_value(): string {
8 | return this._current_value;
9 | }
10 |
11 | get previous_value(): string {
12 | return this._previous_value;
13 | }
14 |
15 | get operation(): string {
16 | return this._operation;
17 | }
18 |
19 | get waiting_for_operand(): boolean {
20 | return this._waiting_for_operand;
21 | }
22 |
23 | input_digit(digit: string): void {
24 | if (this._waiting_for_operand) {
25 | this._current_value = digit;
26 | this._waiting_for_operand = false;
27 | } else {
28 | this._current_value =
29 | this._current_value === '0'
30 | ? digit
31 | : this._current_value + digit;
32 | }
33 | }
34 |
35 | input_operation(next_operation: string): void {
36 | const input_value = parseFloat(this._current_value);
37 |
38 | if (this._previous_value === '') {
39 | this._previous_value = this._current_value;
40 | } else if (this._operation) {
41 | const current_result = this.calculate();
42 | this._current_value = String(current_result);
43 | this._previous_value = this._current_value;
44 | }
45 |
46 | this._waiting_for_operand = true;
47 | this._operation = next_operation;
48 | }
49 |
50 | calculate(): number {
51 | const prev = parseFloat(this._previous_value);
52 | const current = parseFloat(this._current_value);
53 |
54 | if (this._operation === '+') return prev + current;
55 | if (this._operation === '-') return prev - current;
56 | if (this._operation === '*') return prev * current;
57 | if (this._operation === '/') return prev / current;
58 | return current;
59 | }
60 |
61 | perform_calculation(): void {
62 | const result = this.calculate();
63 | this._current_value = String(result);
64 | this._previous_value = '';
65 | this._operation = '';
66 | this._waiting_for_operand = true;
67 | }
68 |
69 | clear(): void {
70 | this._current_value = '0';
71 | this._previous_value = '';
72 | this._operation = '';
73 | this._waiting_for_operand = false;
74 | }
75 |
76 | // For testing purposes
77 | reset(): void {
78 | this.clear();
79 | }
80 | }
81 |
82 | // Export singleton instance
83 | export const calculator_state = new CalculatorState();
84 |
--------------------------------------------------------------------------------
/src/lib/state/form-state.svelte.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 | import {
3 | validate_with_schema,
4 | type ValidationResult,
5 | type ValidationRule,
6 | } from '../utils/validation.ts';
7 |
8 | export interface FormField {
9 | value: string;
10 | validation_rules?: ValidationRule;
11 | validation_result?: ValidationResult;
12 | touched: boolean;
13 | }
14 |
15 | export interface FormState {
16 | [key: string]: FormField;
17 | }
18 |
19 | // Helper function to convert legacy rules to Zod schema
20 | function create_schema_from_rules(
21 | rules: ValidationRule,
22 | ): z.ZodSchema {
23 | if (rules.schema) {
24 | return rules.schema;
25 | }
26 |
27 | // Convert legacy rules to Zod schema
28 | let schema: z.ZodSchema = z.string();
29 |
30 | if (rules.required) {
31 | schema = schema.min(1, 'This field is required');
32 | } else {
33 | schema = schema.optional().or(z.literal(''));
34 | }
35 |
36 | if (rules.min_length) {
37 | schema = schema.min(
38 | rules.min_length,
39 | `Must be at least ${rules.min_length} characters`,
40 | );
41 | }
42 |
43 | if (rules.max_length) {
44 | schema = schema.max(
45 | rules.max_length,
46 | `Must be no more than ${rules.max_length} characters`,
47 | );
48 | }
49 |
50 | if (rules.pattern) {
51 | schema = schema.regex(rules.pattern, 'Invalid format');
52 | }
53 |
54 | return schema;
55 | }
56 |
57 | export function create_form_state(
58 | initial_fields: Record<
59 | string,
60 | { value?: string; validation_rules?: ValidationRule }
61 | >,
62 | ) {
63 | // Initialize form state with runes
64 | const form_state = $state({});
65 |
66 | // Initialize fields
67 | for (const [field_name, config] of Object.entries(initial_fields)) {
68 | form_state[field_name] = {
69 | value: config.value || '',
70 | validation_rules: config.validation_rules,
71 | validation_result: { is_valid: true, error_message: '' },
72 | touched: false,
73 | };
74 | }
75 |
76 | function update_field(field_name: string, value: string) {
77 | if (!form_state[field_name]) return;
78 |
79 | form_state[field_name].value = value;
80 | form_state[field_name].touched = true;
81 |
82 | // Validate if rules exist
83 | if (form_state[field_name].validation_rules) {
84 | const schema = create_schema_from_rules(
85 | form_state[field_name].validation_rules!,
86 | );
87 | form_state[field_name].validation_result = validate_with_schema(
88 | schema,
89 | value,
90 | );
91 | }
92 | }
93 |
94 | function validate_all_fields(): boolean {
95 | let all_valid = true;
96 |
97 | for (const field_name of Object.keys(form_state)) {
98 | const field = form_state[field_name];
99 | if (field.validation_rules) {
100 | const schema = create_schema_from_rules(
101 | field.validation_rules,
102 | );
103 | field.validation_result = validate_with_schema(
104 | schema,
105 | field.value,
106 | );
107 | field.touched = true;
108 |
109 | if (!field.validation_result.is_valid) {
110 | all_valid = false;
111 | }
112 | }
113 | }
114 |
115 | return all_valid;
116 | }
117 |
118 | function reset_form() {
119 | for (const field_name of Object.keys(form_state)) {
120 | form_state[field_name].value = '';
121 | form_state[field_name].touched = false;
122 | form_state[field_name].validation_result = {
123 | is_valid: true,
124 | error_message: '',
125 | };
126 | }
127 | }
128 |
129 | function get_form_data(): Record {
130 | const data: Record = {};
131 | for (const [field_name, field] of Object.entries(form_state)) {
132 | data[field_name] = field.value;
133 | }
134 | return data;
135 | }
136 |
137 | // Derived state using runes
138 | const is_form_valid = $derived(() => {
139 | return Object.values(form_state).every(
140 | (field) =>
141 | !field.validation_rules || field.validation_result?.is_valid,
142 | );
143 | });
144 |
145 | const has_changes = $derived(() => {
146 | return Object.values(form_state).some((field) => field.touched);
147 | });
148 |
149 | const field_errors = $derived(() => {
150 | const errors: Record = {};
151 | for (const [field_name, field] of Object.entries(form_state)) {
152 | if (
153 | field.touched &&
154 | field.validation_result &&
155 | !field.validation_result.is_valid
156 | ) {
157 | errors[field_name] = field.validation_result.error_message;
158 | }
159 | }
160 | return errors;
161 | });
162 |
163 | return {
164 | // State
165 | get form_state() {
166 | return form_state;
167 | },
168 |
169 | // Derived state
170 | get is_form_valid() {
171 | return is_form_valid;
172 | },
173 | get has_changes() {
174 | return has_changes;
175 | },
176 | get field_errors() {
177 | return field_errors;
178 | },
179 |
180 | // Actions
181 | update_field,
182 | validate_all_fields,
183 | reset_form,
184 | get_form_data,
185 | };
186 | }
187 |
--------------------------------------------------------------------------------
/src/lib/state/github-status.svelte.ts:
--------------------------------------------------------------------------------
1 | import { browser } from '$app/environment';
2 |
3 | export interface GitHubStatus {
4 | unit_tests: {
5 | status: 'passing' | 'failing' | 'unknown';
6 | badge_url: string;
7 | };
8 | e2e_tests: {
9 | status: 'passing' | 'failing' | 'unknown';
10 | badge_url: string;
11 | };
12 | }
13 |
14 | interface GitHubStatusState {
15 | data: GitHubStatus | null;
16 | loading: boolean;
17 | error: string | null;
18 | last_updated: number | null;
19 | }
20 |
21 | const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
22 |
23 | export class GitHubStatusManager {
24 | private state = $state({
25 | data: null,
26 | loading: false,
27 | error: null,
28 | last_updated: null,
29 | });
30 |
31 | constructor() {
32 | // Auto-fetch on creation (client-side only)
33 | if (browser) {
34 | this.fetch_status();
35 | }
36 | }
37 |
38 | get data() {
39 | return this.state.data;
40 | }
41 |
42 | get loading() {
43 | return this.state.loading;
44 | }
45 |
46 | get error() {
47 | return this.state.error;
48 | }
49 |
50 | get last_updated() {
51 | return this.state.last_updated;
52 | }
53 |
54 | get overall_status(): 'passing' | 'failing' | 'unknown' {
55 | if (!this.state.data) return 'unknown';
56 |
57 | const { unit_tests, e2e_tests } = this.state.data;
58 |
59 | // If both are passing, overall is passing
60 | if (
61 | unit_tests.status === 'passing' &&
62 | e2e_tests.status === 'passing'
63 | ) {
64 | return 'passing';
65 | }
66 |
67 | // If either is failing, overall is failing
68 | if (
69 | unit_tests.status === 'failing' ||
70 | e2e_tests.status === 'failing'
71 | ) {
72 | return 'failing';
73 | }
74 |
75 | // Otherwise unknown
76 | return 'unknown';
77 | }
78 |
79 | get status_message(): string {
80 | if (!this.state.data) return 'Status unknown';
81 |
82 | const { unit_tests, e2e_tests } = this.state.data;
83 | const overall = this.overall_status;
84 |
85 | if (overall === 'passing') {
86 | return 'All tests passing';
87 | } else if (overall === 'failing') {
88 | // Provide specific failure messages
89 | const unit_failing = unit_tests.status === 'failing';
90 | const e2e_failing = e2e_tests.status === 'failing';
91 |
92 | if (unit_failing && e2e_failing) {
93 | return 'Unit & E2E tests failing';
94 | } else if (unit_failing) {
95 | return 'Unit tests failing';
96 | } else if (e2e_failing) {
97 | return 'E2E tests failing';
98 | } else {
99 | return 'Tests failing';
100 | }
101 | } else {
102 | return 'Test status unknown';
103 | }
104 | }
105 |
106 | get status_color(): 'success' | 'error' | 'warning' {
107 | const overall = this.overall_status;
108 | return overall === 'passing'
109 | ? 'success'
110 | : overall === 'failing'
111 | ? 'error'
112 | : 'warning';
113 | }
114 |
115 | async fetch_status(force_refresh = false) {
116 | if (!browser) return;
117 |
118 | // Don't fetch if we have recent data and not forcing refresh
119 | if (
120 | !force_refresh &&
121 | this.state.data &&
122 | this.state.last_updated
123 | ) {
124 | const time_since_update = Date.now() - this.state.last_updated;
125 | if (time_since_update < CACHE_DURATION) {
126 | return;
127 | }
128 | }
129 |
130 | this.state.loading = true;
131 | this.state.error = null;
132 |
133 | try {
134 | const response = await fetch('/api/github-status');
135 |
136 | // Handle both successful responses and 500s with valid data
137 | if (response.ok || response.status === 500) {
138 | const data: GitHubStatus = await response.json();
139 |
140 | this.state.data = data;
141 | this.state.last_updated = Date.now();
142 | this.state.loading = false;
143 |
144 | // Set error state for 500 responses but still use the data
145 | if (response.status === 500) {
146 | this.state.error = 'GitHub API temporarily unavailable';
147 | console.warn(
148 | 'GitHub status API returned 500, using fallback data',
149 | );
150 | } else {
151 | this.state.error = null;
152 | }
153 | } else {
154 | throw new Error(
155 | `HTTP ${response.status}: ${response.statusText}`,
156 | );
157 | }
158 | } catch (error) {
159 | const error_message =
160 | error instanceof Error
161 | ? error.message
162 | : 'Unknown error occurred';
163 |
164 | this.state.loading = false;
165 | this.state.error = error_message;
166 |
167 | console.error('Failed to fetch GitHub status:', error);
168 | }
169 | }
170 |
171 | refresh() {
172 | return this.fetch_status(true);
173 | }
174 | }
175 |
176 | export const github_status = new GitHubStatusManager();
177 |
--------------------------------------------------------------------------------
/src/lib/utils/highlighter.svelte.ts:
--------------------------------------------------------------------------------
1 | import { browser } from '$app/environment';
2 | import { createHighlighter } from 'shiki';
3 |
4 | // Module-level singleton highlighter (shared across all component instances)
5 | let highlighter_instance: any = null;
6 | let highlighter_promise: Promise | null = null;
7 |
8 | export function get_highlighter(): Promise {
9 | if (!browser) return Promise.resolve(null);
10 |
11 | // Return existing instance if available
12 | if (highlighter_instance) {
13 | return Promise.resolve(highlighter_instance);
14 | }
15 |
16 | // Return existing promise if in progress
17 | if (highlighter_promise) {
18 | return highlighter_promise;
19 | }
20 |
21 | // Create new highlighter promise
22 | highlighter_promise = createHighlighter({
23 | themes: ['night-owl'],
24 | langs: [
25 | 'svelte',
26 | 'typescript',
27 | 'javascript',
28 | 'html',
29 | 'css',
30 | 'json',
31 | 'markdown',
32 | 'python',
33 | 'bash',
34 | ],
35 | }).then((highlighter: any) => {
36 | highlighter_instance = highlighter;
37 | return highlighter;
38 | });
39 |
40 | return highlighter_promise;
41 | }
42 |
--------------------------------------------------------------------------------
/src/lib/utils/untrack-validation.svelte.test.ts:
--------------------------------------------------------------------------------
1 | import { flushSync, untrack } from 'svelte';
2 | import { describe, expect, it, vi } from 'vitest';
3 | import { validate_email, validate_password } from './validation.ts';
4 |
5 | // Mock the validation utilities
6 | vi.mock('../utils/validation.ts', () => ({
7 | validate_email: vi.fn((email: string) => {
8 | const email_pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9 | if (!email.trim()) {
10 | return {
11 | is_valid: false,
12 | error_message: 'This field is required',
13 | };
14 | }
15 | if (!email_pattern.test(email)) {
16 | return { is_valid: false, error_message: 'Invalid format' };
17 | }
18 | return { is_valid: true, error_message: '' };
19 | }),
20 | validate_password: vi.fn((password: string) => {
21 | if (!password.trim()) {
22 | return {
23 | is_valid: false,
24 | error_message: 'This field is required',
25 | };
26 | }
27 | if (password.length < 8) {
28 | return {
29 | is_valid: false,
30 | error_message: 'Must be at least 8 characters',
31 | };
32 | }
33 | return { is_valid: true, error_message: '' };
34 | }),
35 | }));
36 |
37 | describe('Untrack Usage Validation', () => {
38 | describe('Basic $derived with untrack', () => {
39 | it('should access $derived values using untrack', () => {
40 | // Create reactive state in test
41 | let email = $state('');
42 | const email_validation = $derived(validate_email(email));
43 |
44 | // Test invalid email
45 | email = 'invalid-email';
46 | flushSync();
47 |
48 | // ✅ CORRECT: Use untrack to access $derived value
49 | const result = untrack(() => email_validation);
50 | expect(result.is_valid).toBe(false);
51 | expect(result.error_message).toBe('Invalid format');
52 |
53 | // Test valid email
54 | email = 'test@example.com';
55 | flushSync();
56 |
57 | const valid_result = untrack(() => email_validation);
58 | expect(valid_result.is_valid).toBe(true);
59 | expect(valid_result.error_message).toBe('');
60 | });
61 |
62 | it('should handle complex derived logic', () => {
63 | // Recreate login form logic in test
64 | let email = $state('');
65 | let submit_attempted = $state(false);
66 | let email_touched = $state(false);
67 |
68 | const email_validation = $derived(validate_email(email));
69 | const show_email_error = $derived(
70 | submit_attempted || email_touched,
71 | );
72 | const email_error = $derived(
73 | show_email_error && !email_validation.is_valid
74 | ? email_validation.error_message
75 | : '',
76 | );
77 |
78 | // Initially no errors shown
79 | expect(untrack(() => show_email_error)).toBe(false);
80 | expect(untrack(() => email_error)).toBe('');
81 |
82 | // After touching field with invalid email
83 | email = 'invalid';
84 | email_touched = true;
85 | flushSync();
86 |
87 | expect(untrack(() => show_email_error)).toBe(true);
88 | expect(untrack(() => email_error)).toBe('Invalid format');
89 | });
90 |
91 | it('should validate form validity calculation', () => {
92 | let email = $state('');
93 | let password = $state('');
94 |
95 | const email_validation = $derived(validate_email(email));
96 | const password_validation = $derived(
97 | validate_password(password),
98 | );
99 | const form_is_valid = $derived(
100 | email_validation.is_valid && password_validation.is_valid,
101 | );
102 |
103 | // Initially invalid
104 | expect(untrack(() => form_is_valid)).toBe(false);
105 |
106 | // Valid email, invalid password
107 | email = 'test@example.com';
108 | password = '123';
109 | flushSync();
110 |
111 | expect(untrack(() => form_is_valid)).toBe(false);
112 |
113 | // Both valid
114 | password = 'validpassword123';
115 | flushSync();
116 |
117 | expect(untrack(() => form_is_valid)).toBe(true);
118 | });
119 | });
120 |
121 | describe('Why untrack is necessary', () => {
122 | it('should demonstrate untrack prevents reactive dependencies', () => {
123 | let count = $state(0);
124 | let doubled = $derived(count * 2);
125 |
126 | // ✅ Use untrack to read without creating dependencies
127 | expect(untrack(() => doubled)).toBe(0);
128 |
129 | count = 5;
130 | flushSync();
131 |
132 | expect(untrack(() => doubled)).toBe(10);
133 | });
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/src/lib/utils/validation.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const email_schema = z.string().email('Invalid email format');
4 |
5 | export const password_schema = z
6 | .string()
7 | .min(8, 'Password must be at least 8 characters')
8 | .regex(
9 | /[A-Z]/,
10 | 'Password must contain at least one uppercase letter',
11 | )
12 | .regex(
13 | /[a-z]/,
14 | 'Password must contain at least one lowercase letter',
15 | )
16 | .regex(/[0-9]/, 'Password must contain at least one number');
17 |
18 | // Legacy ValidationRule interface for backward compatibility
19 | export interface ValidationRule {
20 | schema?: z.ZodSchema;
21 | required?: boolean;
22 | min_length?: number;
23 | max_length?: number;
24 | pattern?: RegExp;
25 | }
26 |
27 | export interface ValidationResult {
28 | is_valid: boolean;
29 | error_message: string;
30 | }
31 |
32 | // Helper to convert Zod results to ValidationResult format
33 | export function validate_with_schema(
34 | schema: z.ZodSchema,
35 | value: unknown,
36 | ): ValidationResult {
37 | const result = schema.safeParse(value);
38 | return {
39 | is_valid: result.success,
40 | error_message: result.success
41 | ? ''
42 | : result.error.issues[0]?.message || 'Invalid input',
43 | };
44 | }
45 |
46 | // Simplified validation functions
47 | export function validate_email(email: string): ValidationResult {
48 | return validate_with_schema(email_schema, email);
49 | }
50 |
51 | export function validate_password(
52 | password: string,
53 | ): ValidationResult {
54 | return validate_with_schema(password_schema, password);
55 | }
56 |
57 | // Utility functions (not validation-related)
58 | export function format_currency(
59 | amount: number,
60 | currency = 'USD',
61 | ): string {
62 | return new Intl.NumberFormat('en-US', {
63 | style: 'currency',
64 | currency,
65 | }).format(amount);
66 | }
67 |
68 | export function debounce any>(
69 | func: T,
70 | wait: number,
71 | ): (...args: Parameters) => void {
72 | let timeout: ReturnType;
73 | return (...args: Parameters) => {
74 | clearTimeout(timeout);
75 | timeout = setTimeout(() => func(...args), wait);
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/src/routes/+error.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | 404 - Page Not Found | Sveltest
7 |
11 |
12 |
13 |
14 |
15 |
16 |
404
17 |
18 | Page Not Found
19 |
20 |
21 | The page you're looking for doesn't exist or has been moved.
22 |
23 |
24 |
25 |
26 |
Go Home
27 |
28 | Error {$page.status}: {$page.error?.message ||
29 | 'Page not found'}
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 | {@render children?.()}
42 |
43 |
44 |
45 |
48 |
49 |
102 |
103 |
104 |
105 | © {new Date().getFullYear()} Sveltest - Open source testing
106 | resource for Svelte applications
107 |
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/routes/about/+page.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | {title} - Sveltest Docs
13 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/routes/about/+page.ts:
--------------------------------------------------------------------------------
1 | import { error } from '@sveltejs/kit';
2 |
3 | export const load = async () => {
4 | const slug = 'about';
5 | try {
6 | const Copy = await import(`../../copy/${slug}.md`);
7 | return {
8 | Copy: Copy.default,
9 | slug,
10 | };
11 | } catch (e) {
12 | error(404, `Documentation for "${slug}" not found`);
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/routes/api/csp-report/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import type { RequestHandler } from './$types';
3 |
4 | export const POST: RequestHandler = async ({ request }) => {
5 | try {
6 | const report = await request.json();
7 |
8 | // Log CSP violations for debugging
9 | console.warn('🚨 CSP Violation Report:', {
10 | timestamp: new Date().toISOString(),
11 | 'blocked-uri': report['csp-report']?.['blocked-uri'],
12 | 'violated-directive':
13 | report['csp-report']?.['violated-directive'],
14 | 'original-policy': report['csp-report']?.['original-policy'],
15 | 'document-uri': report['csp-report']?.['document-uri'],
16 | referrer: report['csp-report']?.['referrer'],
17 | 'line-number': report['csp-report']?.['line-number'],
18 | 'column-number': report['csp-report']?.['column-number'],
19 | 'source-file': report['csp-report']?.['source-file'],
20 | });
21 |
22 | // In a production app, you might want to:
23 | // - Store violations in a database
24 | // - Send alerts for critical violations
25 | // - Aggregate violation data for analysis
26 | // - Filter out known false positives
27 |
28 | return json({ received: true }, { status: 200 });
29 | } catch (error) {
30 | console.error('Error processing CSP report:', error);
31 | return json({ error: 'Invalid report format' }, { status: 400 });
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/routes/api/csp-report/server.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | afterEach,
3 | beforeEach,
4 | describe,
5 | expect,
6 | it,
7 | vi,
8 | } from 'vitest';
9 | import { POST } from './+server';
10 |
11 | describe('CSP Report API', () => {
12 | let console_warn_spy: any;
13 | let console_error_spy: any;
14 |
15 | beforeEach(() => {
16 | console_warn_spy = vi
17 | .spyOn(console, 'warn')
18 | .mockImplementation(() => {});
19 | console_error_spy = vi
20 | .spyOn(console, 'error')
21 | .mockImplementation(() => {});
22 | });
23 |
24 | afterEach(() => {
25 | console_warn_spy.mockRestore();
26 | console_error_spy.mockRestore();
27 | });
28 |
29 | it('should handle valid CSP violation reports', async () => {
30 | const mock_csp_report = {
31 | 'csp-report': {
32 | 'blocked-uri': 'data:image/svg+xml,... ',
33 | 'violated-directive': 'img-src',
34 | 'original-policy': "default-src 'self'; img-src 'self'",
35 | 'document-uri': 'http://localhost:5173/todos',
36 | referrer: '',
37 | 'line-number': 42,
38 | 'column-number': 15,
39 | 'source-file': 'http://localhost:5173/todos',
40 | },
41 | };
42 |
43 | const request = new Request('http://localhost/api/csp-report', {
44 | method: 'POST',
45 | headers: { 'Content-Type': 'application/json' },
46 | body: JSON.stringify(mock_csp_report),
47 | });
48 |
49 | const response = await POST({ request } as any);
50 | const result = await response.json();
51 |
52 | expect(response.status).toBe(200);
53 | expect(result).toEqual({ received: true });
54 | expect(console_warn_spy).toHaveBeenCalledWith(
55 | '🚨 CSP Violation Report:',
56 | expect.objectContaining({
57 | 'blocked-uri': 'data:image/svg+xml,... ',
58 | 'violated-directive': 'img-src',
59 | timestamp: expect.any(String),
60 | }),
61 | );
62 | });
63 |
64 | it('should handle malformed CSP reports gracefully', async () => {
65 | const request = new Request('http://localhost/api/csp-report', {
66 | method: 'POST',
67 | headers: { 'Content-Type': 'application/json' },
68 | body: 'invalid json',
69 | });
70 |
71 | const response = await POST({ request } as any);
72 | const result = await response.json();
73 |
74 | expect(response.status).toBe(400);
75 | expect(result).toEqual({ error: 'Invalid report format' });
76 | expect(console_error_spy).toHaveBeenCalledWith(
77 | 'Error processing CSP report:',
78 | expect.any(Error),
79 | );
80 | });
81 |
82 | it('should handle empty CSP reports', async () => {
83 | const request = new Request('http://localhost/api/csp-report', {
84 | method: 'POST',
85 | headers: { 'Content-Type': 'application/json' },
86 | body: JSON.stringify({}),
87 | });
88 |
89 | const response = await POST({ request } as any);
90 | const result = await response.json();
91 |
92 | expect(response.status).toBe(200);
93 | expect(result).toEqual({ received: true });
94 | expect(console_warn_spy).toHaveBeenCalledWith(
95 | '🚨 CSP Violation Report:',
96 | expect.objectContaining({
97 | timestamp: expect.any(String),
98 | 'blocked-uri': undefined,
99 | 'violated-directive': undefined,
100 | }),
101 | );
102 | });
103 |
104 | it('should log all relevant CSP report fields', async () => {
105 | const comprehensive_report = {
106 | 'csp-report': {
107 | 'blocked-uri': 'https://evil.com/script.js',
108 | 'violated-directive': 'script-src',
109 | 'original-policy': "default-src 'self'; script-src 'self'",
110 | 'document-uri': 'http://localhost:5173/page',
111 | referrer: 'http://localhost:5173/',
112 | 'line-number': 123,
113 | 'column-number': 45,
114 | 'source-file': 'http://localhost:5173/page',
115 | },
116 | };
117 |
118 | const request = new Request('http://localhost/api/csp-report', {
119 | method: 'POST',
120 | headers: { 'Content-Type': 'application/json' },
121 | body: JSON.stringify(comprehensive_report),
122 | });
123 |
124 | await POST({ request } as any);
125 |
126 | expect(console_warn_spy).toHaveBeenCalledWith(
127 | '🚨 CSP Violation Report:',
128 | expect.objectContaining({
129 | 'blocked-uri': 'https://evil.com/script.js',
130 | 'violated-directive': 'script-src',
131 | 'original-policy': "default-src 'self'; script-src 'self'",
132 | 'document-uri': 'http://localhost:5173/page',
133 | referrer: 'http://localhost:5173/',
134 | 'line-number': 123,
135 | 'column-number': 45,
136 | 'source-file': 'http://localhost:5173/page',
137 | timestamp: expect.any(String),
138 | }),
139 | );
140 | });
141 | });
142 |
--------------------------------------------------------------------------------
/src/routes/api/github-status/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 | import type { RequestHandler } from './$types';
3 |
4 | interface GitHubStatus {
5 | unit_tests: {
6 | status: 'passing' | 'failing' | 'unknown';
7 | badge_url: string;
8 | };
9 | e2e_tests: {
10 | status: 'passing' | 'failing' | 'unknown';
11 | badge_url: string;
12 | };
13 | }
14 |
15 | const GITHUB_REPO = 'spences10/sveltest';
16 | const UNIT_TESTS_WORKFLOW = 'unit-tests.yaml';
17 | const E2E_WORKFLOW = 'e2e.yaml';
18 |
19 | const get_workflow_status = async (
20 | workflow_file: string,
21 | ): Promise<'passing' | 'failing' | 'unknown'> => {
22 | try {
23 | // GitHub Actions API endpoint to get workflow runs
24 | const api_url = `https://api.github.com/repos/${GITHUB_REPO}/actions/workflows/${workflow_file}/runs?per_page=1&status=completed`;
25 |
26 | const response = await fetch(api_url, {
27 | headers: {
28 | Accept: 'application/vnd.github.v3+json',
29 | 'User-Agent': 'Sveltest-App',
30 | },
31 | });
32 |
33 | if (!response.ok) {
34 | console.warn(
35 | `Failed to fetch workflow status for ${workflow_file}:`,
36 | response.status,
37 | );
38 | return 'unknown';
39 | }
40 |
41 | const data = await response.json();
42 |
43 | if (!data.workflow_runs || data.workflow_runs.length === 0) {
44 | return 'unknown';
45 | }
46 |
47 | const latest_run = data.workflow_runs[0];
48 |
49 | // Check if the latest run was successful
50 | return latest_run.conclusion === 'success'
51 | ? 'passing'
52 | : 'failing';
53 | } catch (error) {
54 | console.error(
55 | `Error fetching workflow status for ${workflow_file}:`,
56 | error,
57 | );
58 | return 'unknown';
59 | }
60 | };
61 |
62 | export const GET: RequestHandler = async () => {
63 | try {
64 | const [unit_status, e2e_status] = await Promise.all([
65 | get_workflow_status(UNIT_TESTS_WORKFLOW),
66 | get_workflow_status(E2E_WORKFLOW),
67 | ]);
68 |
69 | // Check if any status is unknown due to errors
70 | const has_errors =
71 | unit_status === 'unknown' || e2e_status === 'unknown';
72 |
73 | const status: GitHubStatus = {
74 | unit_tests: {
75 | status: unit_status,
76 | badge_url: `https://github.com/${GITHUB_REPO}/actions/workflows/${UNIT_TESTS_WORKFLOW}/badge.svg`,
77 | },
78 | e2e_tests: {
79 | status: e2e_status,
80 | badge_url: `https://github.com/${GITHUB_REPO}/actions/workflows/${E2E_WORKFLOW}/badge.svg`,
81 | },
82 | };
83 |
84 | // Return 500 if there were errors fetching status
85 | if (has_errors) {
86 | return json(status, {
87 | status: 500,
88 | headers: {
89 | 'Cache-Control': 'public, max-age=60', // Shorter cache for errors
90 | },
91 | });
92 | }
93 |
94 | return json(status, {
95 | headers: {
96 | 'Cache-Control': 'public, max-age=300', // Cache for 5 minutes
97 | },
98 | });
99 | } catch (error) {
100 | console.error('Error fetching GitHub status:', error);
101 |
102 | // Return fallback status
103 | return json(
104 | {
105 | unit_tests: {
106 | status: 'unknown' as const,
107 | badge_url: `https://github.com/${GITHUB_REPO}/actions/workflows/${UNIT_TESTS_WORKFLOW}/badge.svg`,
108 | },
109 | e2e_tests: {
110 | status: 'unknown' as const,
111 | badge_url: `https://github.com/${GITHUB_REPO}/actions/workflows/${E2E_WORKFLOW}/badge.svg`,
112 | },
113 | },
114 | {
115 | status: 500,
116 | headers: {
117 | 'Cache-Control': 'public, max-age=60', // Shorter cache for errors
118 | },
119 | },
120 | );
121 | }
122 | };
123 |
--------------------------------------------------------------------------------
/src/routes/api/health/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit';
2 |
3 | export async function GET() {
4 | return json({
5 | status: 'ok',
6 | timestamp: new Date().toISOString(),
7 | uptime: process.uptime(),
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/src/routes/api/search/+server.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generate_search_index,
3 | search_full_text,
4 | type SearchIndex,
5 | type SearchIndexItem,
6 | } from '$lib/server/search-index';
7 | import { json } from '@sveltejs/kit';
8 | import type { RequestHandler } from './$types';
9 |
10 | export interface SearchResult {
11 | id: string;
12 | title: string;
13 | description: string;
14 | url: string;
15 | type: 'topic' | 'example' | 'code';
16 | category?: string;
17 | excerpt?: string;
18 | }
19 |
20 | // Full-text search index
21 | let full_search_index: SearchIndex | null = null;
22 |
23 | export const GET: RequestHandler = async ({ url }) => {
24 | const query = url.searchParams.get('q') || '';
25 | const filter = url.searchParams.get('filter') || 'all'; // 'all', 'docs', 'examples', 'components'
26 |
27 | // Build full-text search index once per server instance
28 | if (!full_search_index) {
29 | full_search_index = await generate_search_index();
30 | }
31 |
32 | // Perform full-text search
33 | const results = search_full_text(query, full_search_index, filter);
34 |
35 | // Convert to API format for compatibility with existing components
36 | const api_results: SearchResult[] = results.map(
37 | (item: SearchIndexItem) => ({
38 | id: item.id,
39 | title: item.title,
40 | description: item.description,
41 | url: item.url,
42 | type: item.type,
43 | category: item.category,
44 | excerpt: item.excerpt,
45 | }),
46 | );
47 |
48 | return json({
49 | query,
50 | filter,
51 | results: api_results,
52 | total: api_results.length,
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/src/routes/api/search/server.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from 'vitest';
2 | import { GET } from './+server';
3 |
4 | describe('/api/search', () => {
5 | describe('GET - Full-text search functionality', () => {
6 | test('should return empty results for empty query', async () => {
7 | const url = new URL('http://localhost/api/search?q=');
8 | const response = await GET({ url } as any);
9 | const data = await response.json();
10 |
11 | expect(data.query).toBe('');
12 | expect(data.filter).toBe('all');
13 | expect(data.results).toEqual([]);
14 | expect(data.total).toBe(0);
15 | });
16 |
17 | test('should find "mock" in documentation content', async () => {
18 | const url = new URL('http://localhost/api/search?q=mock');
19 | const response = await GET({ url } as any);
20 | const data = await response.json();
21 |
22 | expect(data.query).toBe('mock');
23 | expect(data.results.length).toBeGreaterThan(0);
24 |
25 | // Should find API Reference and Best Practices (both contain mock content)
26 | const api_reference = data.results.find(
27 | (r: any) => r.id === 'topic-api-reference',
28 | );
29 | const best_practices = data.results.find(
30 | (r: any) => r.id === 'topic-best-practices',
31 | );
32 |
33 | expect(api_reference || best_practices).toBeDefined();
34 | });
35 |
36 | test('should find "vi.fn" in code examples', async () => {
37 | const url = new URL('http://localhost/api/search?q=vi.fn');
38 | const response = await GET({ url } as any);
39 | const data = await response.json();
40 |
41 | expect(data.results.length).toBeGreaterThan(0);
42 |
43 | // Should find results when searching for vi.fn (content is in full text, not just excerpts)
44 | // The search algorithm finds it in the full content and returns relevant results
45 | expect(data.results.length).toBeGreaterThan(0);
46 | });
47 |
48 | test('should handle case-insensitive search', async () => {
49 | const lower_url = new URL('http://localhost/api/search?q=mock');
50 | const upper_url = new URL('http://localhost/api/search?q=MOCK');
51 |
52 | const lower_response = await GET({ url: lower_url } as any);
53 | const upper_response = await GET({ url: upper_url } as any);
54 |
55 | const lower_data = await lower_response.json();
56 | const upper_data = await upper_response.json();
57 |
58 | expect(lower_data.results.length).toBeGreaterThan(0);
59 | expect(upper_data.results.length).toBeGreaterThan(0);
60 | // Should find similar results regardless of case
61 | expect(lower_data.results.length).toBe(
62 | upper_data.results.length,
63 | );
64 | });
65 |
66 | test('should filter results by docs only', async () => {
67 | const url = new URL(
68 | 'http://localhost/api/search?q=test&filter=docs',
69 | );
70 | const response = await GET({ url } as any);
71 | const data = await response.json();
72 |
73 | expect(data.filter).toBe('docs');
74 | // All results should be docs-related
75 | data.results.forEach((result: any) => {
76 | expect(
77 | result.type === 'topic' ||
78 | result.category === 'Documentation' ||
79 | result.category === 'Quick Start',
80 | ).toBe(true);
81 | });
82 | });
83 |
84 | test('should filter results by examples only', async () => {
85 | const url = new URL(
86 | 'http://localhost/api/search?q=test&filter=examples',
87 | );
88 | const response = await GET({ url } as any);
89 | const data = await response.json();
90 |
91 | expect(data.filter).toBe('examples');
92 | // All results should be examples (but not components/docs)
93 | data.results.forEach((result: any) => {
94 | expect(result.type).toBe('example');
95 | expect([
96 | 'Components',
97 | 'Documentation',
98 | 'Quick Start',
99 | ]).not.toContain(result.category);
100 | });
101 | });
102 |
103 | test('should include excerpts for results', async () => {
104 | const url = new URL('http://localhost/api/search?q=component');
105 | const response = await GET({ url } as any);
106 | const data = await response.json();
107 |
108 | if (data.results.length > 0) {
109 | expect(data.results[0]).toHaveProperty('excerpt');
110 | expect(typeof data.results[0].excerpt).toBe('string');
111 | }
112 | });
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/src/routes/api/secure-data/+server.ts:
--------------------------------------------------------------------------------
1 | import { API_SECRET } from '$env/static/private';
2 | import { error, json } from '@sveltejs/kit';
3 |
4 | export const GET = async ({ request }) => {
5 | const auth_token = request.headers.get('authorization');
6 |
7 | if (!auth_token || auth_token !== `Bearer ${API_SECRET}`) {
8 | throw error(401, 'Unauthorized');
9 | }
10 |
11 | return json({
12 | message: 'Secret data retrieved successfully',
13 | data: {
14 | items: ['secret1', 'secret2', 'secret3'],
15 | },
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/src/routes/docs/+page.server.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generate_search_index,
3 | search_full_text,
4 | } from '$lib/server/search-index';
5 | import { fail } from '@sveltejs/kit';
6 | import type { Actions } from './$types';
7 |
8 | export const actions: Actions = {
9 | search: async ({ request }) => {
10 | const form_data = await request.formData();
11 | const query = form_data.get('q')?.toString() || '';
12 | const filter = form_data.get('filter')?.toString() || 'all';
13 |
14 | if (!query.trim()) {
15 | return fail(400, {
16 | error: 'Search query is required',
17 | query,
18 | filter,
19 | });
20 | }
21 |
22 | try {
23 | // Generate search index (cached in production)
24 | const search_index = await generate_search_index();
25 |
26 | // Perform full-text search
27 | const results = search_full_text(query, search_index, filter);
28 |
29 | return {
30 | success: true,
31 | query,
32 | filter,
33 | results: results.slice(0, 10), // Limit results for form response
34 | total: results.length,
35 | search_type: 'server_action',
36 | };
37 | } catch (error) {
38 | console.error('Search error:', error);
39 | return fail(500, {
40 | error: 'Search failed. Please try again.',
41 | query,
42 | filter,
43 | });
44 | }
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/routes/docs/+page.ts:
--------------------------------------------------------------------------------
1 | import { topics } from '$lib/data/topics';
2 | import type { PageLoad } from './$types';
3 |
4 | export const load: PageLoad = async () => {
5 | return {
6 | topics,
7 | };
8 | };
9 |
--------------------------------------------------------------------------------
/src/routes/docs/[topic]/+page.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | {data.topic_info.title} - Sveltest Docs
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {#key data.slug}
24 |
25 |
26 |
27 | {/key}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/routes/docs/[topic]/+page.ts:
--------------------------------------------------------------------------------
1 | import { topics } from '$lib/data/topics';
2 | import { error } from '@sveltejs/kit';
3 |
4 | export const load = async ({ params }) => {
5 | const slug = params.topic || 'getting-started';
6 | const topic_info = topics.find((topic) => topic.slug === slug);
7 |
8 | if (!topic_info) {
9 | error(404, `Documentation for "${slug}" not found`);
10 | }
11 |
12 | try {
13 | const Copy = await import(`../../../copy/${slug}.md`);
14 | return {
15 | Copy: Copy.default,
16 | slug,
17 | topic_info,
18 | };
19 | } catch (e) {
20 | error(404, `Documentation for "${slug}" not found`);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/routes/docs/[topic]/prism.css:
--------------------------------------------------------------------------------
1 | /**
2 | * MIT License
3 | * Copyright (c) 2018 Sarah Drasner
4 | * Sarah Drasner's[@sdras] Night Owl
5 | * Ported by Sara vieria [@SaraVieira]
6 | * Added by Souvik Mandal [@SimpleIndian]
7 | */
8 |
9 | code[class*='language-'],
10 | pre[class*='language-'] {
11 | color: #d6deeb;
12 | font-family:
13 | 'Victor Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono',
14 | monospace;
15 | text-align: left;
16 | white-space: pre;
17 | word-spacing: normal;
18 | word-break: normal;
19 | word-wrap: normal;
20 |
21 | -moz-tab-size: 2;
22 | -o-tab-size: 2;
23 | tab-size: 2;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | pre[class*='language-']::-moz-selection,
32 | pre[class*='language-'] ::-moz-selection,
33 | code[class*='language-']::-moz-selection,
34 | code[class*='language-'] ::-moz-selection {
35 | text-shadow: none;
36 | background: rgba(29, 59, 83, 0.99);
37 | }
38 |
39 | pre[class*='language-']::selection,
40 | pre[class*='language-'] ::selection,
41 | code[class*='language-']::selection,
42 | code[class*='language-'] ::selection {
43 | text-shadow: none;
44 | background: rgba(29, 59, 83, 0.99);
45 | }
46 |
47 | @media print {
48 | code[class*='language-'],
49 | pre[class*='language-'] {
50 | text-shadow: none;
51 | }
52 | }
53 |
54 | /* Code blocks */
55 | pre[class*='language-'] {
56 | /* padding: 1em; */
57 | /* margin: 0.5em 0; */
58 | overflow: auto;
59 | }
60 |
61 | :not(pre) > code[class*='language-'],
62 | pre[class*='language-'] {
63 | color: #d6deeb;
64 | background: #19212e;
65 | }
66 |
67 | :not(pre) > code[class*='language-'] {
68 | /* padding: 0.1em; */
69 | border-radius: 0.3em;
70 | white-space: normal;
71 | }
72 |
73 | .token.comment,
74 | .token.prolog,
75 | .token.cdata {
76 | color: rgb(99, 119, 119);
77 | }
78 |
79 | .token.punctuation {
80 | color: rgb(199, 146, 234);
81 | }
82 |
83 | .namespace {
84 | color: rgb(178, 204, 214);
85 | }
86 |
87 | .token.deleted {
88 | color: rgba(239, 83, 80, 0.56);
89 | font-style: italic;
90 | }
91 |
92 | .token.symbol,
93 | .token.property {
94 | color: rgb(128, 203, 196);
95 | }
96 |
97 | .token.tag,
98 | .token.operator,
99 | .token.keyword {
100 | color: rgb(127, 219, 202);
101 | }
102 |
103 | .token.boolean {
104 | color: rgb(255, 88, 116);
105 | }
106 |
107 | .token.number {
108 | color: rgb(247, 140, 108);
109 | }
110 |
111 | .token.constant,
112 | .token.function,
113 | .token.builtin,
114 | .token.char {
115 | color: rgb(130, 170, 255);
116 | }
117 |
118 | .token.selector,
119 | .token.doctype {
120 | color: rgb(199, 146, 234);
121 | font-style: italic;
122 | }
123 |
124 | .token.attr-name,
125 | .token.inserted {
126 | color: rgb(173, 219, 103);
127 | font-style: italic;
128 | }
129 |
130 | .token.string,
131 | .token.url,
132 | .token.entity,
133 | .language-css .token.string,
134 | .style .token.string {
135 | color: rgb(173, 219, 103);
136 | }
137 |
138 | .token.class-name,
139 | .token.atrule,
140 | .token.attr-value {
141 | color: rgb(255, 203, 139);
142 | }
143 |
144 | .token.regex,
145 | .token.important,
146 | .token.variable {
147 | color: rgb(214, 222, 235);
148 | }
149 |
150 | .token.important,
151 | .token.bold {
152 | font-weight: bold;
153 | }
154 |
155 | .token.italic {
156 | font-style: italic;
157 | }
158 |
--------------------------------------------------------------------------------
/src/routes/examples/todos/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { fail } from '@sveltejs/kit';
2 | import type { Actions, PageServerLoad } from './$types';
3 |
4 | export const load: PageServerLoad = async () => {
5 | // Simulate DB fetch
6 | const todos = [
7 | { id: 1, title: 'Learn SvelteKit', done: false },
8 | { id: 2, title: 'Write tests', done: false },
9 | ];
10 |
11 | return { todos };
12 | };
13 |
14 | export const actions = {
15 | add_todo: async ({ request }) => {
16 | const form_data = await request.formData();
17 | const title = form_data.get('title')?.toString();
18 |
19 | if (!title) {
20 | return fail(400, { error: 'Title is required' });
21 | }
22 |
23 | // Simulate DB insert
24 | const new_todo = {
25 | id: Date.now(),
26 | title,
27 | done: false,
28 | };
29 |
30 | return { success: true, todo: new_todo };
31 | },
32 | } satisfies Actions;
33 |
--------------------------------------------------------------------------------
/src/routes/examples/todos/+page.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | Form Actions Testing - Sveltest
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
24 |
25 |
26 |
29 | Form Actions Testing
30 |
31 |
32 |
33 |
36 | Form Actions Testing
37 |
38 |
41 | Interactive todo demonstration for testing form actions
42 |
43 | Now using local storage with comprehensive testing
45 | capabilities
47 |
48 |
49 |
50 |
51 |
54 |
59 |
60 |
61 |
62 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/routes/examples/todos/page.server.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import type { RequestEvent } from './$types';
3 | import { actions, load } from './+page.server';
4 |
5 | describe('Todos Server', () => {
6 | describe('load function', () => {
7 | it('should return initial todos', async () => {
8 | const response = (await load({ locals: {} } as any)) as {
9 | todos: Array<{ title: string; done: boolean }>;
10 | };
11 |
12 | expect(response.todos).toHaveLength(2);
13 | expect(response.todos[0]).toHaveProperty(
14 | 'title',
15 | 'Learn SvelteKit',
16 | );
17 | });
18 | });
19 |
20 | describe('actions', () => {
21 | it('should add new todo with valid data', async () => {
22 | const form_data = new FormData();
23 | form_data.append('title', 'New Todo');
24 |
25 | const response = await actions.add_todo({
26 | request: new Request('http://localhost', {
27 | method: 'POST',
28 | body: form_data,
29 | }),
30 | url: new URL('http://localhost/examples/todos'),
31 | } as RequestEvent);
32 |
33 | if (!('error' in response) && 'success' in response) {
34 | expect(response.success).toBe(true);
35 | expect(response.todo).toHaveProperty('title', 'New Todo');
36 | expect(response.todo.done).toBe(false);
37 | }
38 | });
39 |
40 | it('should fail with empty title', async () => {
41 | const form_data = new FormData();
42 | form_data.append('title', '');
43 |
44 | const response = await actions.add_todo({
45 | request: new Request('http://localhost', {
46 | method: 'POST',
47 | body: form_data,
48 | }),
49 | url: new URL('http://localhost/examples/todos'),
50 | } as RequestEvent);
51 |
52 | expect(response).toEqual({
53 | status: 400,
54 | data: { error: 'Title is required' },
55 | });
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/routes/examples/unit/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { fail } from '@sveltejs/kit';
2 | import type { Actions } from './$types';
3 |
4 | export const actions = {
5 | calculate: async ({ request }) => {
6 | const data = await request.formData();
7 | const num1 = Number(data.get('num1'));
8 | const num2 = Number(data.get('num2'));
9 |
10 | if (isNaN(num1) || isNaN(num2)) {
11 | return fail(400, { error: 'Please provide valid numbers' });
12 | }
13 |
14 | return { result: num1 + num2 };
15 | },
16 | } satisfies Actions;
17 |
--------------------------------------------------------------------------------
/src/routes/page.ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { render } from 'svelte/server';
2 | import { describe, expect, test } from 'vitest';
3 | import Page from './+page.svelte';
4 |
5 | describe('/+page.svelte SSR', () => {
6 | test('should render HTML correctly on server', () => {
7 | const { body, head } = render(Page);
8 |
9 | // Test that main content is rendered
10 | expect(body).toContain('Sveltest');
11 | expect(body).toContain(
12 | 'A comprehensive collection of testing patterns and',
13 | );
14 | expect(body).toContain('examples');
15 |
16 | // Test that navigation links are present
17 | expect(body).toContain('href="/examples"');
18 | expect(body).toContain('Explore Examples');
19 | expect(body).toContain('href="/todos"');
20 | expect(body).toContain('Try Todo Manager');
21 |
22 | // Test that feature content is rendered
23 | expect(body).toContain('Everything You Need');
24 | expect(body).toContain('Comprehensive testing tools');
25 |
26 | // Test badge content
27 | expect(body).toContain('Battle-Tested in Production');
28 | });
29 |
30 | test('should generate CSS for styling', () => {
31 | const result = render(Page);
32 |
33 | // Svelte 5 render returns { head, html, body } structure
34 | expect(result.head).toBeDefined();
35 | expect(result.html).toBeDefined();
36 | expect(result.body).toBeDefined();
37 |
38 | // All should be strings
39 | expect(typeof result.head).toBe('string');
40 | expect(typeof result.html).toBe('string');
41 | expect(typeof result.body).toBe('string');
42 | });
43 |
44 | test('should render semantic HTML structure', () => {
45 | const { body } = render(Page);
46 |
47 | // Test semantic HTML elements
48 | expect(body).toContain(' {
58 | const { body } = render(Page);
59 |
60 | // Test hero section structure
61 | expect(body).toContain('hero');
62 | expect(body).toContain('bg-gradient-to-br');
63 | expect(body).toContain('min-h-screen');
64 | });
65 |
66 | test('should render feature sections', () => {
67 | const { body } = render(Page);
68 |
69 | // Test feature content that actually exists
70 | expect(body).toContain('Testing Types');
71 | expect(body).toContain('Best Practices');
72 | expect(body).toContain('Real-world Examples');
73 | expect(body).toContain('card');
74 | });
75 |
76 | test('should render without props (static page)', () => {
77 | // This page doesn't require props, should render successfully
78 | expect(() => {
79 | render(Page);
80 | }).not.toThrow();
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/src/routes/page.svelte.test.ts:
--------------------------------------------------------------------------------
1 | // BEFORE: @testing-library/svelte
2 | /*
3 | import { describe, test, expect } from 'vitest';
4 | import '@testing-library/jest-dom/vitest';
5 | import { render, screen } from '@testing-library/svelte';
6 | import Page from './+page.svelte';
7 |
8 | describe('/+page.svelte', () => {
9 | it('should render h1', () => {
10 | render(Page);
11 | expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
12 | });
13 | });
14 | */
15 |
16 | // AFTER: vitest-browser-svelte
17 | import { page } from '@vitest/browser/context';
18 | import { describe, expect, it } from 'vitest';
19 | import { render } from 'vitest-browser-svelte';
20 | import Page from './+page.svelte';
21 |
22 | describe('/+page.svelte', () => {
23 | it('should render h1', async () => {
24 | render(Page);
25 |
26 | const heading = page.getByText('Sveltest').first();
27 | await expect.element(heading).toBeInTheDocument();
28 | });
29 |
30 | describe('AI Rules File Access', () => {
31 | it('should provide Cursor rules file link', async () => {
32 | render(Page);
33 |
34 | const cursor_link = page.getByRole('link', {
35 | name: /View Cursor Rules/i,
36 | });
37 | await expect.element(cursor_link).toBeInTheDocument();
38 | await expect
39 | .element(cursor_link)
40 | .toHaveAttribute(
41 | 'href',
42 | 'https://github.com/spences10/sveltest/blob/main/.cursor/rules/testing.mdc',
43 | );
44 | });
45 |
46 | it('should provide Windsurf rules file link', async () => {
47 | render(Page);
48 |
49 | const windsurf_link = page.getByRole('link', {
50 | name: /View Windsurf Rules/i,
51 | });
52 | await expect.element(windsurf_link).toBeInTheDocument();
53 | await expect
54 | .element(windsurf_link)
55 | .toHaveAttribute(
56 | 'href',
57 | 'https://github.com/spences10/sveltest/blob/main/.windsurf/rules/testing.md',
58 | );
59 | });
60 |
61 | it('should open rules links in new tab with security attributes', async () => {
62 | render(Page);
63 |
64 | const cursor_link = page.getByRole('link', {
65 | name: /View Cursor Rules/i,
66 | });
67 | const windsurf_link = page.getByRole('link', {
68 | name: /View Windsurf Rules/i,
69 | });
70 |
71 | await expect
72 | .element(cursor_link)
73 | .toHaveAttribute('target', '_blank');
74 | await expect
75 | .element(cursor_link)
76 | .toHaveAttribute('rel', 'noopener noreferrer');
77 |
78 | await expect
79 | .element(windsurf_link)
80 | .toHaveAttribute('target', '_blank');
81 | await expect
82 | .element(windsurf_link)
83 | .toHaveAttribute('rel', 'noopener noreferrer');
84 | });
85 |
86 | it('should display AI rules section content', async () => {
87 | render(Page);
88 |
89 | await expect
90 | .element(page.getByText('Cursor Rules').first())
91 | .toBeInTheDocument();
92 | await expect
93 | .element(page.getByText('Windsurf Rules').first())
94 | .toBeInTheDocument();
95 | await expect
96 | .element(
97 | page.getByText(
98 | /Pre-configured AI assistant rules for Cursor/i,
99 | ),
100 | )
101 | .toBeInTheDocument();
102 | await expect
103 | .element(
104 | page.getByText(
105 | /Modern rule system with trigger-based activation for Windsurf/i,
106 | ),
107 | )
108 | .toBeInTheDocument();
109 | });
110 | });
111 | });
112 |
--------------------------------------------------------------------------------
/src/routes/search-example/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | Search Examples - SvelTest
7 |
11 |
12 |
13 |
14 |
Search Examples
15 |
16 |
17 |
18 |
19 |
20 |
Site-wide Search
21 |
22 | Search across all content: documentation, examples, and
23 | components.
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
Documentation Search
37 |
38 | Search only documentation topics and guides.
39 |
40 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
Examples Search
52 |
53 | Search only testing examples and code snippets.
54 |
55 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
Components Search
67 |
68 | Search only UI components and their examples.
69 |
70 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
Keyboard Shortcuts
83 |
84 | Press ⌘ +
85 | K
86 | (or Ctrl +
87 | K ) to focus any search input.
88 |
89 |
90 | Press Esc to clear the search and
91 | unfocus.
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/routes/search-index.json/+server.ts:
--------------------------------------------------------------------------------
1 | import { generate_search_index } from '$lib/server/search-index';
2 | import { json } from '@sveltejs/kit';
3 | import type { RequestHandler } from './$types';
4 |
5 | export const prerender = true;
6 |
7 | export const GET: RequestHandler = async () => {
8 | try {
9 | const search_index = await generate_search_index();
10 |
11 | return json(search_index, {
12 | headers: {
13 | 'Content-Type': 'application/json; charset=utf-8',
14 | 'Cache-Control': 'public, max-age=3600',
15 | 'X-Robots-Tag': 'noindex', // Don't index the search data
16 | },
17 | });
18 | } catch (error) {
19 | console.error('Failed to generate search index:', error);
20 |
21 | return json(
22 | {
23 | items: [],
24 | generated_at: new Date().toISOString(),
25 | total_items: 0,
26 | error: 'Failed to generate search index',
27 | },
28 | {
29 | status: 500,
30 | headers: {
31 | 'Content-Type': 'application/json; charset=utf-8',
32 | },
33 | },
34 | );
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/src/routes/todos/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { fail } from '@sveltejs/kit';
2 | import type { Actions, PageServerLoad } from './$types';
3 |
4 | export const load: PageServerLoad = async ({ locals }) => {
5 | // Simulate DB fetch
6 | const todos = [
7 | { id: 1, title: 'Learn SvelteKit', done: false },
8 | { id: 2, title: 'Write tests', done: false },
9 | ];
10 |
11 | return { todos };
12 | };
13 |
14 | export const actions = {
15 | add_todo: async ({ request }) => {
16 | const form_data = await request.formData();
17 | const title = form_data.get('title')?.toString();
18 |
19 | if (!title) {
20 | return fail(400, { error: 'Title is required' });
21 | }
22 |
23 | // Simulate DB insert
24 | const new_todo = {
25 | id: Date.now(),
26 | title,
27 | done: false,
28 | };
29 |
30 | return { success: true, todo: new_todo };
31 | },
32 | } satisfies Actions;
33 |
--------------------------------------------------------------------------------
/src/routes/todos/+page.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | Todo Manager - Sveltest
14 |
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
✨
34 |
37 | Productivity Suite
38 |
39 |
40 |
41 |
42 |
45 | Todo Manager
46 |
47 |
48 |
51 | A modern, feature-rich todo application with local storage
52 |
53 | Perfect for testing user interactions and data
55 | persistence
57 |
58 |
59 |
60 |
61 |
64 |
65 | Task Management
68 |
69 |
72 |
73 | Local Storage
76 |
77 |
80 |
81 | Progress Tracking
84 |
85 |
88 |
89 | Real-time Updates
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
102 |
107 |
108 |
109 |
110 |
111 |
114 |
115 |
116 |
117 |
118 | Built for Testing
119 |
120 |
121 | This todo manager includes comprehensive test attributes and
122 | is designed to demonstrate various testing scenarios
123 | including user interactions, data persistence, and state
124 | management.
125 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/src/routes/todos/page.server.test.ts:
--------------------------------------------------------------------------------
1 | import type { ActionFailure } from '@sveltejs/kit';
2 | import { describe, expect, it } from 'vitest';
3 | import type { RequestEvent } from './$types';
4 | import { actions, load } from './+page.server';
5 |
6 | describe('Todos Server', () => {
7 | describe('load function', () => {
8 | it('should return initial todos', async () => {
9 | const response = (await load({ locals: {} } as any)) as {
10 | todos: Array<{ title: string; done: boolean }>;
11 | };
12 |
13 | expect(response.todos).toHaveLength(2);
14 | expect(response.todos[0]).toHaveProperty('title');
15 | });
16 | });
17 |
18 | describe('actions', () => {
19 | it('should add new todo with valid data', async () => {
20 | const form_data = new FormData();
21 | form_data.append('title', 'New Todo');
22 |
23 | const response = await actions.add_todo({
24 | request: new Request('http://localhost', {
25 | method: 'POST',
26 | body: form_data,
27 | }),
28 | } as any);
29 |
30 | const success_response = response as {
31 | success: boolean;
32 | todo: { id: number; title: string; done: boolean };
33 | };
34 | expect(success_response.success).toBe(true);
35 | expect(success_response.todo).toHaveProperty(
36 | 'title',
37 | 'New Todo',
38 | );
39 | expect(success_response.todo.done).toBe(false);
40 | });
41 |
42 | it('should fail with empty title', async () => {
43 | const form_data = new FormData();
44 | form_data.append('title', '');
45 |
46 | const response = await actions.add_todo({
47 | request: new Request('http://localhost', {
48 | method: 'POST',
49 | body: form_data,
50 | }),
51 | url: new URL('http://localhost/examples/todos'),
52 | } as RequestEvent);
53 |
54 | const failure_response = response as ActionFailure<{
55 | error: string;
56 | }>;
57 | expect(failure_response.status).toBe(400);
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/src/vitest-setup-client.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spences10/sveltest/4802879e63f98ee2f422c4db975027ca51fa5afa/static/favicon.png
--------------------------------------------------------------------------------
/static/llms.txt:
--------------------------------------------------------------------------------
1 | # Sveltest Testing Documentation
2 |
3 | > 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.
4 |
5 | ## Core Testing Documentation
6 |
7 | ### Getting Started
8 | - **Setup & Installation** - Configure vitest-browser-svelte with multi-project setup supporting the Client-Server Alignment Strategy
9 | - **First Component Test** - Write your first test with locators, event handling, and proper assertions
10 | - **Essential Patterns** - Foundation First approach, always use locators (never containers), handle strict mode violations
11 | - **Common Issues** - Resolve "strict mode violation" errors, role confusion, and form submission hangs
12 |
13 | ### Testing Patterns
14 | - **Component Testing** - Button, input, modal, dropdown patterns with Svelte 5 runes support
15 | - **Locator Strategies** - Semantic queries (preferred), handling multiple elements, role confusion fixes
16 | - **Form Validation** - Complete lifecycle testing (valid → validate → invalid → fix → valid)
17 | - **Integration Testing** - Form submission flows, todo lists, navigation patterns
18 | - **Svelte 5 Runes** - Testing $state, $derived with untrack(), effect patterns
19 | - **SSR Testing** - When to add SSR tests, basic patterns, layout and content validation
20 | - **Server Testing** - API routes with real FormData/Request objects (Client-Server Alignment)
21 |
22 | ### Best Practices
23 | - **Foundation First Approach** - Strategic test planning with describe/it.skip structure
24 | - **Client-Server Alignment Strategy** - Four-layer testing approach minimizing mocking for reliable integration
25 | - **Accessibility Testing** - Semantic queries priority, ARIA testing, keyboard navigation
26 | - **Error Handling** - Robust error testing, edge cases, performance patterns
27 | - **Mocking Strategy** - Smart mocking (external services) vs keeping data contracts real
28 |
29 | ## Additional Resources
30 |
31 | ### Migration & Setup
32 | - **Migration Guide** - Step-by-step migration from @testing-library/svelte with common pitfalls and solutions
33 | - **API Reference** - Complete reference for locators, assertions, interactions, and configuration
34 | - **Troubleshooting** - Common errors, browser issues, mocking problems, and debugging strategies
35 |
36 | ### Advanced Topics
37 | - **E2E Testing** - Complete user journey validation as final safety net for Client-Server Alignment
38 | - **CI/CD** - Production-ready pipelines with Playwright containers, caching, and environment configuration
39 |
40 | ### Project Information
41 | - **About** - Community-driven development, battle-tested production patterns, AI assistant rules
42 |
43 | ## Available LLM Documentation Formats
44 |
45 | ### Primary Documentation
46 | - **Markdown Documentation** - Complete human-readable guides with examples and best practices
47 | - **Testing Rules (MDC)** - Comprehensive AI assistant rules for consistent team adoption across Cursor, Windsurf, and other AI editors
48 |
49 | ### Code Examples
50 | - **Live Component Examples** - Working Svelte components with accompanying test files demonstrating real-world patterns
51 | - **Reference Implementations** - Production-ready code patterns you can copy and adapt
52 |
53 | ### Configuration Templates
54 | - **Vitest Configuration** - Multi-project setup supporting client, server, and SSR testing strategies
55 | - **CI/CD Workflows** - GitHub Actions workflows with Playwright containers and proper caching
56 |
57 | Cross-references: [Getting Started](./getting-started.md) → [Testing Patterns](./testing-patterns.md) → [Best Practices](./best-practices.md) → [Migration Guide](./migration-guide.md) → [API Reference](./api-reference.md)
--------------------------------------------------------------------------------
/static/sveltest.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3 | import { mdsvex } from 'mdsvex';
4 | import autolinkHeadings from 'rehype-autolink-headings';
5 | import rehypeExternalLinks from 'rehype-external-links';
6 | import slugPlugin from 'rehype-slug';
7 |
8 | const config = {
9 | preprocess: [
10 | vitePreprocess(),
11 | mdsvex({
12 | extensions: ['.md'],
13 | smartypants: true,
14 | rehypePlugins: [
15 | slugPlugin,
16 | [
17 | autolinkHeadings,
18 | {
19 | behavior: 'wrap',
20 | },
21 | ],
22 | [
23 | rehypeExternalLinks,
24 | { target: '_blank', rel: 'noopener noreferrer' },
25 | ],
26 | ],
27 | }),
28 | ],
29 | kit: {
30 | adapter: adapter(),
31 | },
32 | extensions: ['.svelte', '.md'],
33 | };
34 |
35 | export default config;
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true
14 | }
15 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
16 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
17 | //
18 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
19 | // from the referenced tsconfig.json - TypeScript does not merge them in
20 | }
21 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import tailwindcss from '@tailwindcss/vite';
3 | import { defineConfig } from 'vite';
4 | import { coverageConfigDefaults } from 'vitest/config';
5 |
6 | export default defineConfig({
7 | plugins: [sveltekit(), tailwindcss()],
8 |
9 | test: {
10 | projects: [
11 | {
12 | // Client-side tests (Svelte components)
13 | extends: './vite.config.ts',
14 | test: {
15 | name: 'client',
16 | environment: 'browser',
17 | // Timeout for browser tests - prevent hanging on element lookups
18 | testTimeout: 2000,
19 | browser: {
20 | enabled: true,
21 | provider: 'playwright',
22 | instances: [
23 | { browser: 'chromium' },
24 | // { browser: 'firefox' },
25 | // { browser: 'webkit' },
26 | ],
27 | },
28 | include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
29 | exclude: [
30 | 'src/lib/server/**',
31 | 'src/**/*.ssr.{test,spec}.{js,ts}',
32 | ],
33 | setupFiles: ['./src/vitest-setup-client.ts'],
34 | },
35 | },
36 | {
37 | // SSR tests (Server-side rendering)
38 | extends: './vite.config.ts',
39 | test: {
40 | name: 'ssr',
41 | environment: 'node',
42 | include: ['src/**/*.ssr.{test,spec}.{js,ts}'],
43 | },
44 | },
45 | {
46 | // Server-side tests (Node.js utilities)
47 | extends: './vite.config.ts',
48 | test: {
49 | name: 'server',
50 | environment: 'node',
51 | include: ['src/**/*.{test,spec}.{js,ts}'],
52 | exclude: [
53 | 'src/**/*.svelte.{test,spec}.{js,ts}',
54 | 'src/**/*.ssr.{test,spec}.{js,ts}',
55 | ],
56 | },
57 | },
58 | ],
59 | coverage: {
60 | all: true,
61 | reporter: ['text-summary', 'html'],
62 | provider: 'v8',
63 | exclude: [
64 | ...coverageConfigDefaults.exclude,
65 | '**/config.{js,ts,cjs}',
66 | '**/*.config.{js,ts,cjs}',
67 | '**/+page.svelte',
68 | '**/+layout.svelte',
69 | '**/+error.svelte',
70 | '.svelte-kit/**',
71 | 'build/**',
72 | 'static/**',
73 | 'dist/**',
74 | 'coverage/**',
75 | '**/*.d.ts',
76 | '**/types/**',
77 | ],
78 | thresholds: {
79 | statements: 50,
80 | branches: 50,
81 | functions: 50,
82 | lines: 50,
83 | },
84 | },
85 | },
86 | });
87 |
--------------------------------------------------------------------------------