├── .cursor └── rules │ └── testing.mdc ├── .cursorrules ├── .github └── workflows │ ├── e2e.yaml │ └── unit-tests.yaml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .windsurf └── rules │ └── testing.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MIGRATION_GUIDE.md ├── README.md ├── documentation-refinement-prompt.md ├── e2e ├── accessibility.spec.ts ├── advanced-scenarios.spec.ts ├── ai-rules.spec.ts ├── api.spec.ts ├── homepage.spec.ts ├── llms-files.spec.ts ├── performance.spec.ts ├── rules-file-access.spec.ts └── smoke-test.spec.ts ├── eslint.config.js ├── llms-txt.md ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── prompts ├── ai-rules.md └── eval-ai-rules.md ├── renovate.json ├── scripts ├── generate-ai-rules.ts ├── generate-llms.ts └── last-evaluation.md ├── src ├── app.css ├── app.d.ts ├── app.html ├── config │ └── anthropic.ts ├── copy │ ├── about.md │ ├── api-reference.md │ ├── best-practices.md │ ├── ci-cd.md │ ├── e2e-testing.md │ ├── getting-started.md │ ├── migration-guide.md │ ├── testing-patterns.md │ └── troubleshooting.md ├── csp-directives.ts ├── hooks.server.test.ts ├── hooks.server.ts ├── lib │ ├── components │ │ ├── button.ssr.test.ts │ │ ├── button.svelte │ │ ├── button.svelte.test.ts │ │ ├── calculator.ssr.test.ts │ │ ├── calculator.svelte │ │ ├── calculator.svelte.test.ts │ │ ├── card.svelte │ │ ├── card.svelte.test.ts │ │ ├── code-block.ssr.test.ts │ │ ├── code-block.svelte │ │ ├── code-block.svelte.test.ts │ │ ├── doc-card.svelte │ │ ├── docs-search.svelte │ │ ├── docs-search.svelte.test.ts │ │ ├── docs-toc.svelte │ │ ├── feature-card.svelte │ │ ├── github-status-pills.svelte │ │ ├── input.ssr.test.ts │ │ ├── input.svelte │ │ ├── input.svelte.test.ts │ │ ├── login-form.svelte │ │ ├── login-form.svelte.test.ts │ │ ├── logo.svelte │ │ ├── modal.svelte │ │ ├── modal.svelte.test.ts │ │ ├── nav.ssr.test.ts │ │ ├── nav.svelte │ │ ├── nav.svelte.test.ts │ │ ├── site-search.svelte │ │ ├── todo-manager.ssr.test.ts │ │ ├── todo-manager.svelte │ │ └── todo-manager.svelte.test.ts │ ├── data │ │ └── topics.ts │ ├── examples │ │ └── code-examples.ts │ ├── icons │ │ ├── arrow.ssr.test.ts │ │ ├── arrow.svelte │ │ ├── arrow.svelte.test.ts │ │ ├── bar-chart.svelte │ │ ├── beaker.svelte │ │ ├── book-open.svelte │ │ ├── calculator.svelte │ │ ├── check-circle.svelte │ │ ├── check.svelte │ │ ├── chevron.svelte │ │ ├── circle-dot.svelte │ │ ├── clipboard.svelte │ │ ├── clock.svelte │ │ ├── code.svelte │ │ ├── cursor.svelte │ │ ├── document.svelte │ │ ├── external-link.svelte │ │ ├── eye-off.svelte │ │ ├── eye.svelte │ │ ├── filter.svelte │ │ ├── github-fork.svelte │ │ ├── github.svelte │ │ ├── heart.svelte │ │ ├── home.svelte │ │ ├── index.ts │ │ ├── lightning-bolt.svelte │ │ ├── menu.svelte │ │ ├── more-vertical.svelte │ │ ├── plus.svelte │ │ ├── robot.svelte │ │ ├── search.svelte │ │ ├── server.svelte │ │ ├── settings.svelte │ │ ├── trash.svelte │ │ ├── user.svelte │ │ ├── windsurf.svelte │ │ ├── x-circle.svelte │ │ └── x.svelte │ ├── index.ts │ ├── server │ │ ├── llms.ts │ │ ├── search-index.test.ts │ │ └── search-index.ts │ ├── state │ │ ├── calculator.svelte.ts │ │ ├── calculator.test.ts │ │ ├── form-state.svelte.ts │ │ ├── form-state.test.ts │ │ ├── github-status.svelte.ts │ │ ├── github-status.test.ts │ │ ├── todo.svelte.ts │ │ └── todo.test.ts │ └── utils │ │ ├── highlighter.svelte.ts │ │ ├── untrack-validation.svelte.test.ts │ │ ├── validation.test.ts │ │ └── validation.ts ├── routes │ ├── +error.svelte │ ├── +layout.svelte │ ├── +page.svelte │ ├── about │ │ ├── +page.svelte │ │ └── +page.ts │ ├── api │ │ ├── csp-report │ │ │ ├── +server.ts │ │ │ └── server.test.ts │ │ ├── github-status │ │ │ ├── +server.ts │ │ │ └── server.test.ts │ │ ├── health │ │ │ └── +server.ts │ │ ├── search │ │ │ ├── +server.ts │ │ │ └── server.test.ts │ │ └── secure-data │ │ │ ├── +server.ts │ │ │ └── server.test.ts │ ├── components │ │ ├── +page.svelte │ │ └── page.svelte.test.ts │ ├── docs │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ ├── +page.ts │ │ ├── [topic] │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ └── prism.css │ │ ├── page.ssr.test.ts │ │ └── page.svelte.test.ts │ ├── examples │ │ ├── +page.svelte │ │ ├── e2e │ │ │ ├── +page.svelte │ │ │ └── page.ssr.test.ts │ │ ├── integration │ │ │ ├── +page.svelte │ │ │ └── page.ssr.test.ts │ │ ├── page.ssr.test.ts │ │ ├── todos │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── page.server.test.ts │ │ └── unit │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── layout.ssr.test.ts │ ├── page.ssr.test.ts │ ├── page.svelte.test.ts │ ├── search-example │ │ └── +page.svelte │ ├── search-index.json │ │ └── +server.ts │ └── todos │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ └── page.server.test.ts └── vitest-setup-client.ts ├── static ├── favicon.png ├── llms-full.txt ├── llms.txt └── sveltest.svg ├── svelte.config.js ├── tsconfig.json └── vite.config.ts /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: E2E Tests 3 | 4 | on: 5 | pull_request: 6 | branches: [main] 7 | push: 8 | branches: [main] 9 | 10 | env: 11 | CI: true 12 | 13 | jobs: 14 | e2e: 15 | name: End-to-End Tests 16 | runs-on: ubuntu-24.04 17 | container: 18 | image: mcr.microsoft.com/playwright:v1.53.1-noble 19 | options: --user 1001 20 | timeout-minutes: 15 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.ref }} 23 | cancel-in-progress: true 24 | env: 25 | API_SECRET: ${{ secrets.API_SECRET }} 26 | PUBLIC_FATHOM_ID: ${{ secrets.PUBLIC_FATHOM_ID }} 27 | PUBLIC_FATHOM_URL: ${{ secrets.PUBLIC_FATHOM_URL }} 28 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 29 | LLM_GEN_SECRET: ${{ secrets.LLM_GEN_SECRET }} 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: '22' 39 | 40 | - name: Setup pnpm 41 | uses: pnpm/action-setup@v4 42 | with: 43 | version: latest 44 | 45 | - name: Get pnpm store directory 46 | shell: bash 47 | run: | 48 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 49 | 50 | - name: Setup pnpm cache 51 | uses: actions/cache@v4 52 | with: 53 | path: ${{ env.STORE_PATH }} 54 | key: 55 | ${{ runner.os }}-pnpm-store-${{ 56 | hashFiles('**/pnpm-lock.yaml') }} 57 | restore-keys: | 58 | ${{ runner.os }}-pnpm-store- 59 | 60 | - name: Install dependencies 61 | run: pnpm install 62 | 63 | - name: Verify Playwright versions match 64 | run: | 65 | # Extract Playwright version from package.json 66 | PACKAGE_VERSION=$(node -p "require('./package.json').devDependencies.playwright.replace(/[\^~]/, '')") 67 | 68 | # Extract version from container image (from this workflow file) 69 | CONTAINER_VERSION=$(grep -o 'playwright:v[0-9.]*' .github/workflows/e2e.yaml | sed 's/playwright:v//') 70 | 71 | echo "📦 Package.json Playwright version: $PACKAGE_VERSION" 72 | echo "🐳 Container image Playwright version: $CONTAINER_VERSION" 73 | 74 | if [ "$PACKAGE_VERSION" != "$CONTAINER_VERSION" ]; then 75 | echo "❌ ERROR: Playwright versions don't match!" 76 | echo " Package.json: $PACKAGE_VERSION" 77 | echo " Container: $CONTAINER_VERSION" 78 | echo " Please update either:" 79 | echo " - package.json devDependencies.playwright to ^$CONTAINER_VERSION" 80 | echo " - Container image to mcr.microsoft.com/playwright:v$PACKAGE_VERSION-noble" 81 | exit 1 82 | else 83 | echo "✅ Playwright versions match: $PACKAGE_VERSION" 84 | fi 85 | 86 | - name: Build application 87 | run: pnpm build 88 | 89 | - name: Run E2E tests 90 | run: pnpm test:e2e 91 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI/CD 3 | 4 | on: 5 | pull_request: 6 | branches: [main] 7 | push: 8 | branches: [main] 9 | 10 | env: 11 | CI: true 12 | 13 | jobs: 14 | test: 15 | name: Unit Tests & Coverage 16 | runs-on: ubuntu-24.04 17 | container: 18 | image: mcr.microsoft.com/playwright:v1.53.1-noble 19 | options: --user 1001 20 | timeout-minutes: 10 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: '22' 33 | 34 | - name: Setup pnpm 35 | uses: pnpm/action-setup@v4 36 | with: 37 | version: latest 38 | 39 | - name: Get pnpm store directory 40 | shell: bash 41 | run: | 42 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 43 | 44 | - name: Setup pnpm cache 45 | uses: actions/cache@v4 46 | with: 47 | path: ${{ env.STORE_PATH }} 48 | key: 49 | ${{ runner.os }}-pnpm-store-${{ 50 | hashFiles('**/pnpm-lock.yaml') }} 51 | restore-keys: | 52 | ${{ runner.os }}-pnpm-store- 53 | 54 | - name: Install dependencies 55 | run: pnpm install 56 | 57 | - name: Verify Playwright versions match 58 | run: | 59 | # Extract Playwright version from package.json 60 | PACKAGE_VERSION=$(node -p "require('./package.json').devDependencies.playwright.replace(/[\^~]/, '')") 61 | 62 | # Extract version from container image (from this workflow file) 63 | CONTAINER_VERSION=$(grep -o 'playwright:v[0-9.]*' .github/workflows/unit-tests.yaml | sed 's/playwright:v//') 64 | 65 | echo "📦 Package.json Playwright version: $PACKAGE_VERSION" 66 | echo "🐳 Container image Playwright version: $CONTAINER_VERSION" 67 | 68 | if [ "$PACKAGE_VERSION" != "$CONTAINER_VERSION" ]; then 69 | echo "❌ ERROR: Playwright versions don't match!" 70 | echo " Package.json: $PACKAGE_VERSION" 71 | echo " Container: $CONTAINER_VERSION" 72 | echo " Please update either:" 73 | echo " - package.json devDependencies.playwright to ^$CONTAINER_VERSION" 74 | echo " - Container image to mcr.microsoft.com/playwright:v$PACKAGE_VERSION-noble" 75 | exit 1 76 | else 77 | echo "✅ Playwright versions match: $PACKAGE_VERSION" 78 | fi 79 | 80 | - name: Build application 81 | run: pnpm build 82 | env: 83 | API_SECRET: ${{ secrets.API_SECRET }} 84 | PUBLIC_FATHOM_ID: ${{ secrets.PUBLIC_FATHOM_ID }} 85 | PUBLIC_FATHOM_URL: ${{ secrets.PUBLIC_FATHOM_URL }} 86 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 87 | LLM_GEN_SECRET: ${{ secrets.LLM_GEN_SECRET }} 88 | 89 | - name: Coverage 90 | run: pnpm test:unit --run --coverage 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test-results 2 | node_modules 3 | 4 | # Output 5 | .output 6 | .vercel 7 | .netlify 8 | .wrangler 9 | /.svelte-kit 10 | /build 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Env 17 | .env 18 | .env.* 19 | !.env.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | 26 | # coverage 27 | coverage 28 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 70, 6 | "proseWrap": "always", 7 | "plugins": [ 8 | "prettier-plugin-svelte", 9 | "prettier-plugin-tailwindcss" 10 | ], 11 | "overrides": [ 12 | { 13 | "files": "*.svelte", 14 | "options": { 15 | "parser": "svelte" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Svelte Code of Conduct 2 | 3 | This project is a Svelte Community project and therefore uses the same 4 | [Code of Conduct](https://github.com/sveltejs/community/blob/main/CODE_OF_CONDUCT.md) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Scott Spence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /e2e/ai-rules.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('AI Rules File Access', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | }); 7 | 8 | test.describe('Rules File Links', () => { 9 | test('should display both Cursor and Windsurf rule cards', async ({ 10 | page, 11 | }) => { 12 | // Use .first() to handle multiple elements with same text 13 | await expect( 14 | page.getByText('Cursor Rules').first(), 15 | ).toBeVisible(); 16 | await expect( 17 | page.getByText('Windsurf Rules').first(), 18 | ).toBeVisible(); 19 | }); 20 | 21 | test('should have functional links to rules files', async ({ 22 | page, 23 | }) => { 24 | const cursor_link = page.getByRole('link', { 25 | name: /View Cursor Rules/i, 26 | }); 27 | const windsurf_link = page.getByRole('link', { 28 | name: /View Windsurf Rules/i, 29 | }); 30 | 31 | await expect(cursor_link).toBeVisible(); 32 | await expect(windsurf_link).toBeVisible(); 33 | }); 34 | }); 35 | 36 | test.describe('GitHub Links Verification', () => { 37 | test('should have correct URLs and attributes', async ({ 38 | page, 39 | }) => { 40 | const cursor_link = page.getByRole('link', { 41 | name: /View Cursor Rules/i, 42 | }); 43 | const windsurf_link = page.getByRole('link', { 44 | name: /View Windsurf Rules/i, 45 | }); 46 | 47 | // Verify URLs 48 | await expect(cursor_link).toHaveAttribute( 49 | 'href', 50 | 'https://github.com/spences10/sveltest/blob/main/.cursor/rules/testing.mdc', 51 | ); 52 | await expect(windsurf_link).toHaveAttribute( 53 | 'href', 54 | 'https://github.com/spences10/sveltest/blob/main/.windsurf/rules/testing.md', 55 | ); 56 | 57 | // Verify security attributes 58 | await expect(cursor_link).toHaveAttribute('target', '_blank'); 59 | await expect(cursor_link).toHaveAttribute( 60 | 'rel', 61 | 'noopener noreferrer', 62 | ); 63 | await expect(windsurf_link).toHaveAttribute('target', '_blank'); 64 | await expect(windsurf_link).toHaveAttribute( 65 | 'rel', 66 | 'noopener noreferrer', 67 | ); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /e2e/homepage.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('Homepage', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | }); 7 | 8 | test('should display main heading and navigation', async ({ 9 | page, 10 | }) => { 11 | await test.step('Check main heading', async () => { 12 | // Be specific about which heading we want - the main title 13 | await expect( 14 | page.getByRole('heading', { name: 'Sveltest' }), 15 | ).toBeVisible(); 16 | }); 17 | 18 | await test.step('Check navigation elements', async () => { 19 | // ✅ Use .first() to handle multiple "Explore Examples" links 20 | await expect( 21 | page.getByRole('link', { name: 'Explore Examples' }).first(), 22 | ).toBeVisible(); 23 | await expect( 24 | page.getByRole('link', { name: 'Try Todo Manager' }), 25 | ).toBeVisible(); 26 | }); 27 | 28 | await test.step('Verify page structure', async () => { 29 | // Check for main content using semantic query 30 | await expect(page.getByRole('main').first()).toBeVisible(); 31 | }); 32 | }); 33 | 34 | test('should navigate to examples page', async ({ page }) => { 35 | await test.step('Click examples link', async () => { 36 | // ✅ Use .first() to handle multiple "Explore Examples" links 37 | await page 38 | .getByRole('link', { name: 'Explore Examples' }) 39 | .first() 40 | .click(); 41 | }); 42 | 43 | await test.step('Verify navigation to examples page', async () => { 44 | await expect(page).toHaveURL('/examples'); 45 | // Be specific about the examples page main heading 46 | await expect( 47 | page.getByRole('heading', { name: 'Testing Patterns' }), 48 | ).toBeVisible(); 49 | }); 50 | }); 51 | 52 | test('should navigate to todos page', async ({ page }) => { 53 | await test.step('Click todos link', async () => { 54 | await page 55 | .getByRole('link', { name: 'Try Todo Manager' }) 56 | .click(); 57 | }); 58 | 59 | await test.step('Verify navigation to todos page', async () => { 60 | await expect(page).toHaveURL('/todos'); 61 | // Be specific about the todos page main heading 62 | await expect( 63 | page.getByRole('heading', { name: 'Todo Manager' }), 64 | ).toBeVisible(); 65 | }); 66 | }); 67 | 68 | test('should have correct page title and meta information', async ({ 69 | page, 70 | }) => { 71 | await test.step('Check page title', async () => { 72 | await expect(page).toHaveTitle(/Sveltest/); 73 | }); 74 | 75 | await test.step('Check meta description', async () => { 76 | const metaDescription = page.locator( 77 | 'meta[name="description"]', 78 | ); 79 | await expect(metaDescription).toHaveAttribute( 80 | 'content', 81 | /testing patterns/i, 82 | ); 83 | }); 84 | }); 85 | 86 | test('should be responsive and mobile-friendly', async ({ 87 | page, 88 | }) => { 89 | await test.step('Test mobile viewport', async () => { 90 | await page.setViewportSize({ width: 375, height: 667 }); 91 | await expect( 92 | page.getByRole('heading', { name: 'Sveltest' }), 93 | ).toBeVisible(); 94 | }); 95 | 96 | await test.step('Test tablet viewport', async () => { 97 | await page.setViewportSize({ width: 768, height: 1024 }); 98 | await expect( 99 | page.getByRole('heading', { name: 'Sveltest' }), 100 | ).toBeVisible(); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /e2e/llms-files.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('LLMs Files Accessibility', () => { 4 | test('should serve llms.txt with correct content type', async ({ 5 | page, 6 | }) => { 7 | const response = await page.goto('/llms.txt'); 8 | 9 | expect(response?.status()).toBe(200); 10 | expect(response?.headers()['content-type']).toContain( 11 | 'text/plain', 12 | ); 13 | 14 | const content = await response?.text(); 15 | expect(content).toContain('# Sveltest Testing Documentation'); 16 | expect(content).toContain('vitest-browser-svelte'); 17 | }); 18 | 19 | test('should serve llms-full.txt with complete documentation', async ({ 20 | page, 21 | }) => { 22 | const response = await page.goto('/llms-full.txt'); 23 | 24 | expect(response?.status()).toBe(200); 25 | expect(response?.headers()['content-type']).toContain( 26 | 'text/plain', 27 | ); 28 | 29 | const content = await response?.text(); 30 | expect(content).toContain('# Sveltest Testing Documentation'); 31 | expect(content).toContain('# Getting Started'); 32 | expect(content).toContain('# Testing Patterns'); 33 | expect(content).toContain('# Best Practices'); 34 | // Verify it's the full content, not just the index 35 | expect(content?.length).toBeGreaterThan(10000); 36 | }); 37 | 38 | test('should have working links to llms files in documentation', async ({ 39 | page, 40 | }) => { 41 | await page.goto('/docs'); 42 | 43 | // Find the LLMs section links 44 | const llmsIndexLink = page.getByRole('link', { 45 | name: 'LLMs Index', 46 | }); 47 | const llmsFullLink = page.getByRole('link', { 48 | name: 'Full Documentation', 49 | }); 50 | 51 | await expect(llmsIndexLink).toBeVisible(); 52 | await expect(llmsFullLink).toBeVisible(); 53 | 54 | // Verify links have correct hrefs 55 | await expect(llmsIndexLink).toHaveAttribute('href', '/llms.txt'); 56 | await expect(llmsFullLink).toHaveAttribute( 57 | 'href', 58 | '/llms-full.txt', 59 | ); 60 | }); 61 | 62 | test('should open llms files in new tab/window', async ({ 63 | page, 64 | context, 65 | }) => { 66 | await page.goto('/docs'); 67 | 68 | // Test that clicking opens in new tab (target="_blank") 69 | const llmsIndexLink = page.getByRole('link', { 70 | name: 'LLMs Index', 71 | }); 72 | await expect(llmsIndexLink).toHaveAttribute('target', '_blank'); 73 | 74 | // Test actual navigation by intercepting the request 75 | const [newPage] = await Promise.all([ 76 | context.waitForEvent('page'), 77 | llmsIndexLink.click(), 78 | ]); 79 | 80 | await newPage.waitForLoadState(); 81 | expect(newPage.url()).toBe( 82 | page.url().replace('/docs', '/llms.txt'), 83 | ); 84 | 85 | const content = await newPage.content(); 86 | expect(content).toContain('# Sveltest Testing Documentation'); 87 | }); 88 | 89 | test('should serve static files with appropriate headers', async ({ 90 | page, 91 | }) => { 92 | const response = await page.goto('/llms.txt'); 93 | 94 | expect(response?.status()).toBe(200); 95 | 96 | // Check basic headers are present 97 | const headers = response?.headers(); 98 | expect(headers).toBeDefined(); 99 | 100 | // At minimum should have content-type 101 | expect(headers?.['content-type']).toContain('text/plain'); 102 | 103 | // Cache control may or may not be set (depends on server config) 104 | // But if present, should be reasonable 105 | const cacheControl = headers?.['cache-control']; 106 | if (cacheControl) { 107 | expect(cacheControl).toMatch(/max-age|public|private|no-cache/); 108 | } 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /e2e/rules-file-access.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('Rules File Access via Deployed Project', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | }); 7 | 8 | test.describe('Rules File Links Visibility', () => { 9 | test('should display both Cursor and Windsurf rules cards', async ({ 10 | page, 11 | }) => { 12 | // Use .first() to handle multiple elements with same text 13 | await expect( 14 | page.getByText('Cursor Rules').first(), 15 | ).toBeVisible(); 16 | await expect( 17 | page.getByText('Windsurf Rules').first(), 18 | ).toBeVisible(); 19 | }); 20 | 21 | test('should have accessible View Rules buttons', async ({ 22 | page, 23 | }) => { 24 | const cursor_button = page.getByRole('link', { 25 | name: /View Cursor Rules/i, 26 | }); 27 | const windsurf_button = page.getByRole('link', { 28 | name: /View Windsurf Rules/i, 29 | }); 30 | 31 | await expect(cursor_button).toBeVisible(); 32 | await expect(windsurf_button).toBeVisible(); 33 | }); 34 | }); 35 | 36 | test.describe('GitHub Rules File Links', () => { 37 | test('should have correct URLs for rules files', async ({ 38 | page, 39 | }) => { 40 | // Cursor rules file link 41 | const cursor_link = page.getByRole('link', { 42 | name: /View Cursor Rules/i, 43 | }); 44 | await expect(cursor_link).toHaveAttribute( 45 | 'href', 46 | 'https://github.com/spences10/sveltest/blob/main/.cursor/rules/testing.mdc', 47 | ); 48 | 49 | // Windsurf rules file link 50 | const windsurf_link = page.getByRole('link', { 51 | name: /View Windsurf Rules/i, 52 | }); 53 | await expect(windsurf_link).toHaveAttribute( 54 | 'href', 55 | 'https://github.com/spences10/sveltest/blob/main/.windsurf/rules/testing.md', 56 | ); 57 | }); 58 | 59 | test('should open links in new tab with security attributes', async ({ 60 | page, 61 | }) => { 62 | const cursor_link = page.getByRole('link', { 63 | name: /View Cursor Rules/i, 64 | }); 65 | const windsurf_link = page.getByRole('link', { 66 | name: /View Windsurf Rules/i, 67 | }); 68 | 69 | // Security and UX attributes for external links 70 | await expect(cursor_link).toHaveAttribute('target', '_blank'); 71 | await expect(cursor_link).toHaveAttribute( 72 | 'rel', 73 | 'noopener noreferrer', 74 | ); 75 | 76 | await expect(windsurf_link).toHaveAttribute('target', '_blank'); 77 | await expect(windsurf_link).toHaveAttribute( 78 | 'rel', 79 | 'noopener noreferrer', 80 | ); 81 | }); 82 | }); 83 | 84 | test.describe('User Journey', () => { 85 | test('should provide clear path to access rules files', async ({ 86 | page, 87 | }) => { 88 | // User can see the rules cards 89 | await expect( 90 | page.getByText('Cursor Rules').first(), 91 | ).toBeVisible(); 92 | await expect( 93 | page.getByText('Windsurf Rules').first(), 94 | ).toBeVisible(); 95 | 96 | // User can access the links (they exist and are clickable) 97 | const cursor_link = page.getByRole('link', { 98 | name: /View Cursor Rules/i, 99 | }); 100 | const windsurf_link = page.getByRole('link', { 101 | name: /View Windsurf Rules/i, 102 | }); 103 | 104 | await expect(cursor_link).toBeEnabled(); 105 | await expect(windsurf_link).toBeEnabled(); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { includeIgnoreFile } from '@eslint/compat'; 2 | import js from '@eslint/js'; 3 | import prettier from 'eslint-config-prettier'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | import svelteConfig from './svelte.config.js'; 9 | const gitignorePath = fileURLToPath( 10 | new URL('./.gitignore', import.meta.url), 11 | ); 12 | 13 | export default ts.config( 14 | includeIgnoreFile(gitignorePath), 15 | js.configs.recommended, 16 | ...ts.configs.recommended, 17 | ...svelte.configs.recommended, 18 | prettier, 19 | ...svelte.configs.prettier, 20 | { 21 | languageOptions: { 22 | globals: { 23 | ...globals.browser, 24 | ...globals.node, 25 | }, 26 | }, 27 | }, 28 | { 29 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 30 | ignores: ['eslint.config.js', 'svelte.config.js'], 31 | 32 | languageOptions: { 33 | parserOptions: { 34 | projectService: true, 35 | extraFileExtensions: ['.svelte'], 36 | parser: ts.parser, 37 | svelteConfig, 38 | }, 39 | }, 40 | }, 41 | ); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltest", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/spences10/sveltest.git" 9 | }, 10 | "homepage": "https://github.com/spences10/sveltest", 11 | "bugs": { 12 | "url": "https://github.com/spences10/sveltest/issues" 13 | }, 14 | "scripts": { 15 | "dev": "vite dev", 16 | "build": "vite build", 17 | "preview": "vite preview", 18 | "prepare": "svelte-kit sync || echo ''", 19 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 20 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 21 | "format": "prettier --write .", 22 | "lint": "prettier --check . && eslint .", 23 | "lint:fix": "prettier --write . && eslint . --fix", 24 | "test:unit": "vitest", 25 | "test:server": "vitest --project=server", 26 | "test:client": "vitest --project=client", 27 | "test:ssr": "vitest --project=ssr", 28 | "test": "npm run test:unit -- --run && npm run test:e2e", 29 | "test:e2e": "playwright test", 30 | "coverage": "vitest --run --coverage", 31 | "generate:llms": "tsx scripts/generate-llms.ts", 32 | "generate:ai-rules": "tsx scripts/generate-ai-rules.ts", 33 | "test:ai-rules": "tsx scripts/generate-ai-rules.ts --dry-run" 34 | }, 35 | "devDependencies": { 36 | "@anthropic-ai/sdk": "^0.55.0", 37 | "@eslint/compat": "^1.3.1", 38 | "@eslint/js": "^9.30.1", 39 | "@fontsource-variable/inter": "^5.2.6", 40 | "@fontsource-variable/victor-mono": "^5.2.6", 41 | "@playwright/test": "^1.53.2", 42 | "@sveltejs/adapter-auto": "^6.0.1", 43 | "@sveltejs/kit": "^2.22.2", 44 | "@sveltejs/vite-plugin-svelte": "^5.1.0", 45 | "@tailwindcss/typography": "^0.5.16", 46 | "@tailwindcss/vite": "^4.1.11", 47 | "@types/node": "^24.0.10", 48 | "@vitest/browser": "^3.2.4", 49 | "@vitest/coverage-v8": "^3.2.4", 50 | "daisyui": "^5.0.43", 51 | "eslint": "^9.30.1", 52 | "eslint-config-prettier": "^10.1.5", 53 | "eslint-plugin-svelte": "^3.10.1", 54 | "fathom-client": "^3.7.2", 55 | "globals": "^16.3.0", 56 | "jsdom": "^26.1.0", 57 | "mdsvex": "^0.12.6", 58 | "playwright": "^1.53.2", 59 | "prettier": "^3.6.2", 60 | "prettier-plugin-svelte": "^3.4.0", 61 | "prettier-plugin-tailwindcss": "^0.6.13", 62 | "rehype-autolink-headings": "^7.1.0", 63 | "rehype-external-links": "^3.0.0", 64 | "rehype-slug": "^6.0.0", 65 | "shiki": "^3.7.0", 66 | "svelte": "^5.35.0", 67 | "svelte-check": "^4.2.2", 68 | "tailwindcss": "^4.1.11", 69 | "tsx": "^4.20.3", 70 | "typescript": "^5.8.3", 71 | "typescript-eslint": "^8.35.1", 72 | "vite": "^7.0.0", 73 | "vitest": "^3.2.4", 74 | "vitest-browser-svelte": "^1.0.0", 75 | "zod": "^3.25.68" 76 | }, 77 | "pnpm": { 78 | "onlyBuiltDependencies": [ 79 | "@tailwindcss/oxide", 80 | "esbuild" 81 | ] 82 | } 83 | } -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173, 7 | }, 8 | 9 | testDir: 'e2e', 10 | }); 11 | -------------------------------------------------------------------------------- /prompts/ai-rules.md: -------------------------------------------------------------------------------- 1 | Generate AI assistant rules for Cursor/Windsurf from the provided 2 | Sveltest documentation. 3 | 4 | ⚠️ CRITICAL INSTRUCTION - READ CAREFULLY ⚠️ 5 | 6 | You must return ONLY the rules content starting with the YAML 7 | frontmatter. DO NOT include: 8 | 9 | - Any introductory text like "Looking at this comprehensive..." 10 | - Section headers like "## Cursor Version" 11 | - Explanatory commentary 12 | - Meta-text about what you're doing 13 | - Code fence markdown blocks 14 | 15 | ## Start your response IMMEDIATELY with: 16 | 17 | description: Comprehensive Testing Best Practices for Svelte 5 + 18 | vitest-browser-svelte globs: 19 | **/\*.test.ts,**/_.svelte.test.ts,\*\*/_.ssr.test.ts alwaysApply: 20 | false 21 | 22 | --- 23 | 24 | Then continue with the actual rules content. NOTHING ELSE. 25 | 26 | TARGET: Maximum 5500 characters (strict limit - will be rejected if 27 | exceeded) 28 | 29 | OUTPUT FORMAT: For Cursor (.cursorrules), include YAML frontmatter: 30 | 31 | ``` 32 | --- 33 | description: Comprehensive Testing Best Practices for Svelte 5 + vitest-browser-svelte 34 | globs: **/*.test.ts,**/*.svelte.test.ts,**/*.ssr.test.ts 35 | alwaysApply: false 36 | --- 37 | 38 | [Rules content here] 39 | ``` 40 | 41 | For Windsurf (.windsurfrules), provide plain text rules only (no 42 | frontmatter). 43 | 44 | REQUIRED SECTIONS: 45 | 46 | - Technology stack identification 47 | - Core testing principles 48 | - Essential patterns and imports 49 | - Critical gotchas and solutions 50 | - Code examples (ultra-compressed) 51 | - Quality standards 52 | 53 | RULES FORMAT: 54 | 55 | ``` 56 | You are an expert in Svelte 5, SvelteKit, TypeScript, and vitest-browser-svelte testing. 57 | 58 | ## Core Principles 59 | [Condensed testing principles] 60 | 61 | ## Essential Patterns 62 | [Key imports and setup patterns] 63 | 64 | ## Critical Rules 65 | [Must-follow rules with brief examples] 66 | 67 | ## Common Errors & Solutions 68 | [Top gotchas with fixes] 69 | ``` 70 | 71 | COMPRESSION REQUIREMENTS (CRITICAL): 72 | 73 | - Every character must add value - NO exceptions 74 | - Use bullet points over paragraphs - ALWAYS 75 | - Combine related concepts - MANDATORY 76 | - Essential patterns only - NO nice-to-haves 77 | - No redundant explanations - NONE 78 | - Focus on actionable rules - ONLY actionables 79 | - Remove all verbose text and examples 80 | - Use ultra-short variable names in examples 81 | - Combine multiple concepts per bullet point 82 | - NO section introductions or explanations 83 | 84 | QUALITY STANDARDS: 85 | 86 | - All code examples must be complete and runnable 87 | - Emphasize `page.getBy*()` locators over containers 88 | - Include `await expect.element()` syntax 89 | - Show proper import statements 90 | - Warn about form submission gotchas 91 | 92 | CRITICAL: If output exceeds 5500 characters, compress further by: 93 | 94 | - Removing all example comments 95 | - Using single-letter variable names 96 | - Combining bullets into single lines 97 | - Removing section headers if needed 98 | 99 | Generate ultra-compressed rules that fit the limit. 100 | -------------------------------------------------------------------------------- /prompts/eval-ai-rules.md: -------------------------------------------------------------------------------- 1 | Evaluate the generated AI assistant rules for quality and compliance. 2 | 3 | EVALUATION CRITERIA: 4 | 5 | ## Character Limit Compliance 6 | 7 | - ✅/❌ Under 6000 characters total 8 | - ✅/❌ No unnecessary verbose explanations 9 | - ✅/❌ Efficient use of space 10 | 11 | ## Content Requirements 12 | 13 | - ✅/❌ Technology stack clearly identified 14 | - ✅/❌ Core testing principles included 15 | - ✅/❌ Essential imports and setup patterns 16 | - ✅/❌ Critical gotchas with solutions 17 | - ✅/❌ Code examples are complete and runnable 18 | - ✅/❌ Quality standards defined 19 | 20 | ## Technical Accuracy 21 | 22 | - ✅/❌ Emphasizes `page.getBy*()` over containers 23 | - ✅/❌ Uses `await expect.element()` syntax 24 | - ✅/❌ Includes proper import statements 25 | - ✅/❌ Warns about form submission issues 26 | - ✅/❌ Svelte 5 + vitest-browser-svelte specific 27 | 28 | ## AI Assistant Compatibility 29 | 30 | - ✅/❌ Clear, actionable rules format 31 | - ✅/❌ Logical organization for AI consumption 32 | - ✅/❌ No ambiguous or conflicting guidance 33 | - ✅/❌ Suitable for Cursor/Windsurf integration 34 | 35 | SCORING SYSTEM (deduct points): 36 | 37 | - DEDUCT 3 points: Over 6000 characters 38 | - DEDUCT 2 points: Missing core technology identification 39 | - DEDUCT 2 points: Missing essential patterns section 40 | - DEDUCT 2 points: Incomplete or non-runnable code examples 41 | - DEDUCT 1 point: Missing import statements in examples 42 | - DEDUCT 1 point: Uses containers instead of locators 43 | - DEDUCT 1 point: Missing form submission warnings 44 | - DEDUCT 1 point: Poor organization for AI consumption 45 | 46 | REQUIRED OUTPUT: 47 | 48 | 1. CHARACTER COUNT: Exact count and pass/fail 49 | 2. CALCULATED SCORE: Base 10, minus deductions (minimum 1) 50 | 3. COMPLIANCE CHECKLIST: All criteria marked ✅/❌ 51 | 4. CRITICAL ISSUES: Must-fix problems for AI assistant use 52 | 5. RECOMMENDATIONS: Specific improvements needed 53 | 54 | Evaluate the following AI rules content: 55 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"] 3 | } 4 | -------------------------------------------------------------------------------- /scripts/last-evaluation.md: -------------------------------------------------------------------------------- 1 | ## EVALUATION RESULTS 2 | 3 | ### 1. CHARACTER COUNT 4 | - **Cursor Version**: 4,847 characters ✅ PASS 5 | - **Windsurf Version**: 4,669 characters ✅ PASS 6 | - Both versions are well under the 6000 character limit 7 | 8 | ### 2. CALCULATED SCORE 9 | **Base Score: 10** 10 | **Deductions: -1 point** 11 | - Missing form submission warnings (partial coverage only) 12 | 13 | **FINAL SCORE: 9/10** 14 | 15 | ### 3. COMPLIANCE CHECKLIST 16 | 17 | #### Character Limit Compliance 18 | - ✅ Under 6000 characters total 19 | - ✅ No unnecessary verbose explanations 20 | - ✅ Efficient use of space 21 | 22 | #### Content Requirements 23 | - ✅ Technology stack clearly identified (Svelte 5, SvelteKit, TypeScript, vitest-browser-svelte) 24 | - ✅ Core testing principles included (5 clear principles) 25 | - ✅ Essential imports and setup patterns (comprehensive import block) 26 | - ❌ Critical gotchas with solutions (partial - missing some key warnings) 27 | - ✅ Code examples are complete and runnable 28 | - ✅ Quality standards defined (code style section) 29 | 30 | #### Technical Accuracy 31 | - ✅ Emphasizes `page.getBy*()` over containers (explicitly warns against containers) 32 | - ✅ Uses `await expect.element()` syntax (correct syntax shown) 33 | - ✅ Includes proper import statements (complete import block) 34 | - ❌ Warns about form submission issues (mentions hanging but incomplete coverage) 35 | - ✅ Svelte 5 + vitest-browser-svelte specific (targeted content) 36 | 37 | #### AI Assistant Compatibility 38 | - ✅ Clear, actionable rules format 39 | - ✅ Logical organization for AI consumption 40 | - ✅ No ambiguous or conflicting guidance 41 | - ✅ Suitable for Cursor/Windsurf integration 42 | 43 | ### 4. CRITICAL ISSUES 44 | **None** - The rules are ready for AI assistant use. 45 | 46 | ### 5. RECOMMENDATIONS 47 | 48 | #### Minor Improvements Needed: 49 | 50 | 1. **Expand Form Submission Warnings** 51 | ```typescript 52 | // Add to Common Errors section: 53 | ### Form Submission Race Conditions 54 | // ❌ Race condition - form submits before assertions 55 | await page.getByRole('button', { name: 'Submit' }).click(); 56 | await expect.element(page.getByText('Success')).toBeVisible(); 57 | 58 | // ✅ Wait for navigation or use force 59 | await page.getByRole('button', { name: 'Submit' }).click({ force: true }); 60 | ``` 61 | 62 | 2. **Add SSR Testing Pattern** 63 | ```typescript 64 | // Add brief SSR testing mention: 65 | ### SSR Tests 66 | // ✅ Test server-side rendering 67 | const html = render.toString(Component, { props }); 68 | expect(html).toContain('expected content'); 69 | ``` 70 | 71 | #### Strengths: 72 | - Excellent organization with clear hierarchy 73 | - Strong emphasis on locators vs containers 74 | - Comprehensive error handling examples 75 | - Perfect balance of brevity and completeness 76 | - Great Svelte 5 runes coverage with `untrack()` 77 | - Practical form validation lifecycle examples 78 | 79 | **OVERALL ASSESSMENT**: Excellent quality AI assistant rules. The content is technically accurate, well-organized, and perfectly sized for AI consumption. The minor missing form submission edge cases don't significantly impact usability. -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | /* Import fonts */ 2 | @import '@fontsource-variable/victor-mono'; 3 | @import '@fontsource-variable/inter'; 4 | 5 | @import 'tailwindcss'; 6 | 7 | /* Plugin configurations */ 8 | @plugin "@tailwindcss/typography"; 9 | @plugin "daisyui"; 10 | 11 | @theme { 12 | --font-sans: 'Inter Variable', sans-serif; 13 | --font-mono: 'Victor Mono Variable', monospace; 14 | } 15 | 16 | @layer utilities { 17 | /* Handle headers with rehype-autolink-headings 'wrap' behavior */ 18 | /* Structure:

content

*/ 19 | .prose h1 a, 20 | .prose h1 { 21 | @apply from-primary via-secondary to-accent bg-gradient-to-r bg-clip-text leading-normal font-black tracking-wide text-transparent drop-shadow-lg; 22 | } 23 | 24 | .prose h2 a, 25 | .prose h2 { 26 | @apply text-secondary font-bold tracking-tight drop-shadow-sm; 27 | } 28 | 29 | .prose h3 a, 30 | .prose h3 { 31 | @apply text-secondary font-semibold tracking-tight; 32 | } 33 | 34 | .prose h4 a, 35 | .prose h5 a, 36 | .prose h6 a, 37 | .prose h4, 38 | .prose h5, 39 | .prose h6 { 40 | @apply text-secondary font-medium; 41 | } 42 | 43 | /* Ensure anchor links inside headers don't show underlines */ 44 | .prose h1 a, 45 | .prose h2 a, 46 | .prose h3 a, 47 | .prose h4 a, 48 | .prose h5 a, 49 | .prose h6 a { 50 | @apply no-underline; 51 | text-decoration: none; 52 | } 53 | 54 | /* Add some nice text effects for emphasis */ 55 | .prose strong { 56 | @apply text-accent font-bold; 57 | } 58 | 59 | .prose em { 60 | @apply text-secondary italic; 61 | } 62 | 63 | .prose code { 64 | @apply text-lg; 65 | } 66 | 67 | .prose blockquote { 68 | @apply border-primary bg-primary/40 text-primary-content border-l-4 italic; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | 18 | 22 | %sveltekit.head% 23 | 24 | 25 |
%sveltekit.body%
26 | 27 | 28 | -------------------------------------------------------------------------------- /src/config/anthropic.ts: -------------------------------------------------------------------------------- 1 | // Shared Anthropic configuration for all scripts and server code 2 | export const ANTHROPIC_CONFIG = { 3 | model: 'claude-sonnet-4-20250514', 4 | generation: { 5 | max_tokens: 32000, 6 | stream: true, 7 | }, 8 | evaluation: { 9 | max_tokens: 32000, 10 | stream: true, 11 | }, 12 | } as const; 13 | -------------------------------------------------------------------------------- /src/copy/about.md: -------------------------------------------------------------------------------- 1 | # About Sveltest 2 | 3 | ## Built for Teams, Battle-Tested in Production 4 | 5 | Sveltest began as a simple weekend project - just a collection of 6 | testing examples to accompany my blog post about migrating from 7 | `@testing-library/svelte` to `vitest-browser-svelte`. But like the 8 | best side projects, it took on a life of its own. 9 | 10 | ## From Weekend Project to Production Patterns 11 | 12 | What started as basic examples quickly evolved into something much 13 | more comprehensive. As I refined these testing approaches with my team 14 | on a large monorepo, the patterns became more sophisticated, more 15 | battle-tested, and more valuable to share with the broader community. 16 | 17 | We're operating at the bleeding edge of Svelte 5 testing - using 18 | `vitest-browser-svelte` in real production environments, discovering 19 | edge cases, and developing patterns that actually work when you scale 20 | them up. These aren't theoretical examples; they're patterns we use 21 | every day to ship reliable software. 22 | 23 | The **Client-Server Alignment Strategy** emerged from real production 24 | pain points where heavily mocked tests passed but production failed 25 | due to client-server contract mismatches. This approach ensures your 26 | tests catch the integration issues that matter. 27 | 28 | ## A Living Documentation Project 29 | 30 | Every component, every test, every pattern in Sveltest serves as a 31 | working example. The beautiful site you're browsing? That's just a 32 | delightful side effect of building comprehensive testing examples. The 33 | real value is in the code itself - patterns you can copy, adapt, and 34 | use in your own projects. 35 | 36 | ## Empowering Teams with AI 37 | 38 | One of the most exciting outcomes has been creating comprehensive AI 39 | assistant rules that help entire teams adopt these testing 40 | methodologies. Whether you're using Cursor, Windsurf, or other 41 | AI-powered editors, these rules ensure consistent, high-quality 42 | testing patterns across your team. 43 | 44 | ## Community-Driven Development 45 | 46 | Sveltest is more than just a personal project - it's a **community 47 | resource** built by developers, for developers. The patterns, 48 | examples, and documentation you see here represent collective wisdom 49 | from teams working with Svelte 5 and modern testing tools in 50 | production environments. 51 | 52 | ### Open Source & Collaborative 53 | 54 | The entire project is 55 | [open source on GitHub](https://github.com/spences10/sveltest), where 56 | you can: 57 | 58 | - **Contribute new testing patterns** from your own projects 59 | - **Report issues** or suggest improvements 60 | - **Share knowledge** through discussions and examples 61 | - **Help improve documentation** for the community 62 | 63 | Every contribution, whether it's a bug report, a new testing example, 64 | or documentation improvement, makes the resource better for everyone 65 | in the Svelte ecosystem. 66 | 67 | ### Your Testing Patterns Matter 68 | 69 | Have you discovered a testing pattern that works well in your project? 70 | Found a better way to test a specific Svelte 5 feature? Encountered an 71 | edge case that others should know about? **Your experience can help 72 | other developers.** 73 | 74 | The best testing resources come from real-world usage, and every team 75 | brings unique challenges and solutions. By sharing your patterns and 76 | contributing to the project, you help build the most comprehensive 77 | Svelte testing resource available. 78 | 79 | ## Why Share This? 80 | 81 | The Svelte ecosystem deserves modern, production-ready testing 82 | patterns. Too many teams struggle with testing because the examples 83 | they find are either too simple for real-world use or too complex to 84 | understand. Sveltest bridges that gap. 85 | 86 | Every bug we catch in production, every edge case we handle, every 87 | pattern we refine - it all makes its way back into these examples. 88 | This isn't just documentation; it's a living repository of testing 89 | wisdom that evolves with real-world usage. 90 | 91 | ## Built for the Community 92 | 93 | Whether you're a solo developer learning testing patterns, a team lead 94 | establishing standards, or an engineering manager looking for proven 95 | approaches, Sveltest provides the examples and patterns you need. 96 | 97 | The code speaks for itself, the tests prove the patterns work, and the 98 | AI rules help your team maintain consistency. This is testing 99 | documentation for the modern web development era. 100 | 101 | --- 102 | 103 | _Sveltest represents the testing patterns and approaches I wish had 104 | existed when I started my Svelte testing journey. Now they're here for 105 | you - and with your contributions, they can be even better for the 106 | next developer._ 107 | 108 | **Ready to contribute?** Visit the 109 | [GitHub repository](https://github.com/spences10/sveltest) to get 110 | started. 111 | -------------------------------------------------------------------------------- /src/copy/e2e-testing.md: -------------------------------------------------------------------------------- 1 | # E2E Testing 2 | 3 | ## The Final Safety Net 4 | 5 | E2E testing completes the **Client-Server Alignment Strategy** by 6 | testing the full user journey from browser to server and back. 7 | 8 | ## Quick Overview 9 | 10 | E2E tests validate: 11 | 12 | - Complete form submission flows 13 | - Client-server integration 14 | - Real network requests 15 | - Full user workflows 16 | 17 | ## Basic Pattern 18 | 19 | ```typescript 20 | // e2e/registration.spec.ts 21 | import { test, expect } from '@playwright/test'; 22 | 23 | test('user registration flow', async ({ page }) => { 24 | await page.goto('/register'); 25 | 26 | await page.getByLabelText('Email').fill('user@example.com'); 27 | await page.getByLabelText('Password').fill('secure123'); 28 | await page.getByRole('button', { name: 'Register' }).click(); 29 | 30 | // Tests the complete client-server integration 31 | await expect(page.getByText('Welcome!')).toBeVisible(); 32 | }); 33 | ``` 34 | 35 | ## Why E2E Matters 36 | 37 | - Catches client-server contract mismatches that unit tests miss 38 | - Validates real form submissions with actual FormData 39 | - Tests complete user workflows 40 | - Provides confidence in production deployments 41 | 42 | --- 43 | 44 | _This document will be expanded with comprehensive E2E patterns, 45 | configuration, and best practices._ 46 | -------------------------------------------------------------------------------- /src/csp-directives.ts: -------------------------------------------------------------------------------- 1 | export const csp_directives = { 2 | 'default-src': ["'self'"], 3 | 'script-src': [ 4 | "'self'", 5 | "'unsafe-inline'", 6 | "'unsafe-eval'", 7 | "'wasm-unsafe-eval'", 8 | 'https://cdn.usefathom.com', 9 | ], 10 | 'script-src-elem': [ 11 | "'self'", 12 | "'unsafe-inline'", 13 | 'https://cdn.usefathom.com', 14 | ], 15 | 'style-src': ["'self'", "'unsafe-inline'"], 16 | 'img-src': ["'self'", 'data:', 'https://cdn.usefathom.com'], 17 | 'font-src': ["'self'", 'data:'], 18 | 'connect-src': [ 19 | "'self'", 20 | 'https://api.usefathom.com', 21 | 'https://*.usefathom.com', 22 | ], 23 | 'frame-src': ["'none'"], 24 | 'object-src': ["'none'"], 25 | 'base-uri': ["'self'"], 26 | 'form-action': ["'self'"], 27 | 'frame-ancestors': ["'none'"], 28 | 'upgrade-insecure-requests': [], 29 | 'report-uri': ['/api/csp-report'], 30 | } as const; 31 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit'; 2 | import { sequence } from '@sveltejs/kit/hooks'; 3 | import { csp_directives } from './csp-directives'; 4 | 5 | // Handle function for security headers 6 | export const handle_security_headers: Handle = async ({ 7 | event, 8 | resolve, 9 | }) => { 10 | const response = await resolve(event); 11 | const headers = new Headers(response.headers); 12 | 13 | // Basic security headers 14 | headers.set('X-Frame-Options', 'SAMEORIGIN'); 15 | headers.set('X-Content-Type-Options', 'nosniff'); 16 | headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); 17 | headers.set( 18 | 'Permissions-Policy', 19 | 'camera=(), microphone=(), geolocation=()', 20 | ); 21 | 22 | // Build CSP directive string 23 | const csp_directive_string = Object.entries(csp_directives) 24 | .map(([key, values]) => { 25 | if (values.length === 0) { 26 | return key; // For directives like 'upgrade-insecure-requests' 27 | } 28 | return `${key} ${values.join(' ')}`; 29 | }) 30 | .join('; '); 31 | 32 | headers.set('Content-Security-Policy', csp_directive_string); 33 | 34 | return new Response(response.body, { 35 | headers, 36 | status: response.status, 37 | statusText: response.statusText, 38 | }); 39 | }; 40 | 41 | // Handle function for error handling 42 | export const handle_errors: Handle = async ({ event, resolve }) => { 43 | try { 44 | return await resolve(event); 45 | } catch (err: any) { 46 | // In a real app, you'd want proper logging here 47 | console.error('Request handler error:', { 48 | message: err.message, 49 | stack: err.stack, 50 | path: event.url.pathname, 51 | method: event.request.method, 52 | }); 53 | 54 | throw err; 55 | } 56 | }; 57 | 58 | // Combine all handle functions in sequence 59 | export const handle = sequence( 60 | handle_security_headers, 61 | handle_errors, 62 | ); 63 | -------------------------------------------------------------------------------- /src/lib/components/button.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 57 | -------------------------------------------------------------------------------- /src/lib/components/calculator.ssr.test.ts: -------------------------------------------------------------------------------- 1 | import { render } from 'svelte/server'; 2 | import { describe, expect, test, vi } from 'vitest'; 3 | import Calculator from './calculator.svelte'; 4 | 5 | // Mock the calculator state for SSR testing 6 | vi.mock('$lib/state/calculator.svelte.ts', () => ({ 7 | calculator_state: { 8 | current_value: '0', 9 | clear: vi.fn(), 10 | input_digit: vi.fn(), 11 | input_operation: vi.fn(), 12 | perform_calculation: vi.fn(), 13 | reset: vi.fn(), 14 | }, 15 | })); 16 | 17 | describe('Calculator SSR', () => { 18 | test('should render without errors', () => { 19 | expect(() => { 20 | render(Calculator); 21 | }).not.toThrow(); 22 | }); 23 | 24 | test('should render essential calculator structure for SEO', () => { 25 | const { body } = render(Calculator); 26 | 27 | // Test core calculator structure is present 28 | expect(body).toContain('class="space-y-6"'); // Main container 29 | expect(body).toContain('class="grid grid-cols-4 gap-2"'); // Button grid 30 | expect(body).toContain('0'); // Default display value 31 | }); 32 | 33 | test('should render all calculator buttons in HTML', () => { 34 | const { body } = render(Calculator); 35 | 36 | // Test digit buttons are rendered 37 | for (let i = 0; i <= 9; i++) { 38 | expect(body).toContain(`>${i}<`); 39 | } 40 | 41 | // Test operation buttons are rendered 42 | expect(body).toContain('>+<'); 43 | expect(body).toContain('>−<'); 44 | expect(body).toContain('>×<'); 45 | expect(body).toContain('>÷<'); 46 | 47 | // Test control buttons are rendered 48 | expect(body).toContain('>C<'); 49 | expect(body).toContain('>=<'); 50 | expect(body).toContain('>.<'); 51 | }); 52 | 53 | test('should render proper button structure with classes', () => { 54 | const { body } = render(Calculator); 55 | 56 | // Test button classes are applied 57 | expect(body).toContain('btn btn-outline btn-sm'); // Clear button 58 | expect(body).toContain('btn btn-ghost btn-sm'); // Digit buttons 59 | expect(body).toContain('btn btn-warning btn-sm'); // Operation buttons 60 | expect(body).toContain('btn btn-primary btn-sm'); // Equals button 61 | }); 62 | 63 | test('should render display area with proper styling', () => { 64 | const { body } = render(Calculator); 65 | 66 | // Test display area structure 67 | expect(body).toContain( 68 | 'bg-base-300/50 mb-4 rounded-lg p-4 text-right font-mono text-3xl', 69 | ); 70 | }); 71 | 72 | test('should render disabled buttons correctly', () => { 73 | const { body } = render(Calculator); 74 | 75 | // Test disabled buttons (± and %) are rendered with disabled attribute 76 | expect(body).toContain('disabled'); 77 | expect(body).toContain('>±<'); 78 | expect(body).toContain('>%<'); 79 | }); 80 | 81 | test('should have proper semantic HTML structure', () => { 82 | const { body } = render(Calculator); 83 | 84 | // Test semantic button elements 85 | expect(body).toMatch(/]*>C<\/button>/); 86 | expect(body).toMatch(/]*>=<\/button>/); 87 | expect(body).toMatch(/]*>\+<\/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 | 22 | 23 | 24 | 30 | 31 | 37 | 43 | 49 | 55 | 56 | 62 | 68 | 74 | 80 | 81 | 87 | 93 | 99 | 105 | 106 | 112 | 118 | 124 |
125 |
126 |
127 | -------------------------------------------------------------------------------- /src/lib/components/card.svelte: -------------------------------------------------------------------------------- 1 | 107 | 108 | 109 |
120 | {#if image_src} 121 | {image_alt} 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 |
149 |

{footer_text}

150 |
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 | 53 | 54 | 65 | -------------------------------------------------------------------------------- /src/lib/components/feature-card.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 |
48 |
51 |
52 |
55 | 56 |
57 |

{title}

58 |

59 | {description} 60 |

61 | {#if badges.length > 0} 62 |
63 | {#each badges as badge} 64 |
65 | {badge.text} 66 |
67 | {/each} 68 |
69 | {/if} 70 | {#if show_button && href && button_text} 71 | 79 | 80 | {button_text} 81 | {#if !button_icon} 82 | {#if is_external_url(href)} 83 | 84 | {:else} 85 | 86 | {/if} 87 | {/if} 88 | 89 | {/if} 90 |
91 |
92 |
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 | 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 | 86 | 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 |