# Phase 9: CI Pipeline Hardening - Research **Researched:** 2026-02-03 **Domain:** Testing infrastructure (Vitest, Playwright, svelte-check) + CI/CD (Gitea Actions) **Confidence:** HIGH ## Summary This phase establishes a comprehensive testing pipeline that blocks deployment on test or type failures. The research covers three testing layers: unit tests (Vitest), type checking (svelte-check), and E2E tests (Playwright), integrated into the existing Gitea Actions workflow. The standard approach for SvelteKit testing in 2026 uses Vitest with browser mode for component tests (real browser via Playwright, not jsdom), traditional Vitest for server/utility tests, and standalone Playwright for full E2E tests. The user decisions lock in 80% coverage threshold, Playwright component tests for DOM interactions, and Slack notifications on failure. Key finding: Vitest browser mode with `vitest-browser-svelte` is the modern approach for Svelte 5 component testing, replacing the older jsdom + @testing-library/svelte pattern. This provides real browser testing with runes support (`.svelte.test.ts` files). **Primary recommendation:** Use multi-project Vitest configuration separating client (browser mode) and server (node) tests, with standalone Playwright for E2E, all gated before Docker build in CI. ## Standard Stack The established libraries/tools for this domain: ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | vitest | ^3.x | Unit/component test runner | Official SvelteKit recommendation, Vite-native | | @vitest/browser | ^3.x | Browser mode for component tests | Real browser testing without jsdom limitations | | vitest-browser-svelte | ^0.x | Svelte component rendering in browser mode | Official Svelte 5 support with runes | | @vitest/browser-playwright | ^3.x | Playwright provider for Vitest browser mode | Real Chrome DevTools Protocol, not simulated events | | @vitest/coverage-v8 | ^3.x | V8-based coverage collection | Fast native coverage, identical accuracy to Istanbul since v3.2 | | @playwright/test | ^1.58 | E2E test framework | Already installed, mature E2E solution | | svelte-check | ^4.x | TypeScript/Svelte type checking | Already installed, CI-compatible output | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | @testing-library/svelte | ^5.x | Alternative component testing | Only if not using browser mode (jsdom fallback) | | drizzle-seed | ^0.x | Database seeding for tests | E2E test fixtures with Drizzle ORM | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | Vitest browser mode | jsdom + @testing-library/svelte | jsdom simulates browser, misses real CSS/runes issues | | v8 coverage | istanbul | istanbul 300% slower, v8 now equally accurate | | Playwright for E2E | Cypress | Playwright already in project, better multi-browser support | **Installation:** ```bash npm install -D vitest @vitest/browser vitest-browser-svelte @vitest/browser-playwright @vitest/coverage-v8 drizzle-seed npx playwright install chromium ``` ## Architecture Patterns ### Recommended Project Structure ``` src/ ├── lib/ │ ├── components/ │ │ ├── Button.svelte │ │ └── Button.svelte.test.ts # Component tests (browser mode) │ ├── utils/ │ │ ├── format.ts │ │ └── format.test.ts # Utility tests (node mode) │ └── server/ │ ├── db/ │ │ └── queries.test.ts # Server tests (node mode) │ └── api.test.ts ├── routes/ │ └── +page.server.test.ts # Server route tests (node mode) tests/ ├── e2e/ # Playwright E2E tests │ ├── fixtures/ │ │ └── db.ts # Database seeding fixture │ ├── user-journeys.spec.ts │ └── index.ts # Custom test with fixtures ├── docker-deployment.spec.ts # Existing deployment tests vitest-setup-client.ts # Browser mode setup vitest.config.ts # Multi-project config (or in vite.config.ts) playwright.config.ts # E2E config (already exists) ``` ### Pattern 1: Multi-Project Vitest Configuration **What:** Separate test projects for different environments (browser vs node) **When to use:** SvelteKit apps with both client components and server code **Example:** ```typescript // vite.config.ts // Source: https://scottspence.com/posts/testing-with-vitest-browser-svelte-guide import { sveltekit } from '@sveltejs/kit/vite'; import tailwindcss from '@tailwindcss/vite'; import { playwright } from '@vitest/browser-playwright'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [tailwindcss(), sveltekit()], test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], include: ['src/**/*.{ts,svelte}'], exclude: ['src/**/*.test.ts', 'src/**/*.spec.ts'], thresholds: { global: { statements: 80, branches: 80, functions: 80, lines: 80, }, }, }, projects: [ { extends: true, test: { name: 'client', testTimeout: 5000, browser: { enabled: true, provider: playwright(), instances: [{ browser: 'chromium' }], }, include: ['src/**/*.svelte.{test,spec}.{js,ts}'], setupFiles: ['./vitest-setup-client.ts'], }, }, { extends: true, test: { name: 'server', environment: 'node', include: ['src/**/*.{test,spec}.{js,ts}'], exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'], }, }, ], }, }); ``` ### Pattern 2: Component Test with Runes Support **What:** Test Svelte 5 components with $state and $derived in real browser **When to use:** Any component using Svelte 5 runes **Example:** ```typescript // src/lib/components/Counter.svelte.test.ts // Source: https://svelte.dev/docs/svelte/testing import { render } from 'vitest-browser-svelte'; import { page } from '@vitest/browser/context'; import { describe, expect, it } from 'vitest'; import { flushSync } from 'svelte'; import Counter from './Counter.svelte'; describe('Counter Component', () => { it('increments count on click', async () => { render(Counter, { props: { initial: 0 } }); const button = page.getByRole('button', { name: /increment/i }); await button.click(); // flushSync needed for external state changes flushSync(); await expect.element(page.getByText('Count: 1')).toBeInTheDocument(); }); }); ``` ### Pattern 3: E2E Database Fixture with Drizzle **What:** Seed database before tests, clean up after **When to use:** E2E tests requiring known data state **Example:** ```typescript // tests/e2e/fixtures/db.ts // Source: https://mainmatter.com/blog/2025/08/21/mock-database-in-svelte-tests/ import { test as base } from '@playwright/test'; import { db } from '../../../src/lib/server/db/index.js'; import * as schema from '../../../src/lib/server/db/schema.js'; import { reset, seed } from 'drizzle-seed'; export const test = base.extend<{ seededDb: typeof db; }>({ seededDb: async ({}, use) => { // Seed with known test data await seed(db, schema, { count: 10 }); await use(db); // Clean up after test await reset(db, schema); }, }); export { expect } from '@playwright/test'; ``` ### Pattern 4: SvelteKit Module Mocking **What:** Mock $app/stores and $app/navigation in unit tests **When to use:** Testing components that use SvelteKit-specific imports **Example:** ```typescript // vitest-setup-client.ts // Source: https://www.closingtags.com/blog/mocking-svelte-stores-in-vitest /// import { vi } from 'vitest'; import { writable } from 'svelte/store'; // Mock $app/navigation vi.mock('$app/navigation', () => ({ goto: vi.fn(() => Promise.resolve()), invalidate: vi.fn(() => Promise.resolve()), invalidateAll: vi.fn(() => Promise.resolve()), beforeNavigate: vi.fn(), afterNavigate: vi.fn(), })); // Mock $app/stores vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost'), params: {}, route: { id: null }, status: 200, error: null, data: {}, form: null, }), navigating: writable(null), updated: { check: vi.fn(), subscribe: writable(false).subscribe }, })); ``` ### Anti-Patterns to Avoid - **Testing with jsdom for Svelte 5 components:** jsdom cannot properly handle runes reactivity. Use browser mode instead. - **Parallel E2E tests with shared database:** Will cause race conditions. Set `workers: 1` in playwright.config.ts. - **Using deprecated @playwright/experimental-ct-svelte:** Use vitest-browser-svelte instead for component tests. - **Mocking everything in E2E tests:** E2E tests should test real integrations. Only mock external services if necessary. ## Don't Hand-Roll Problems that look simple but have existing solutions: | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Coverage collection | Custom instrumentation | @vitest/coverage-v8 | Handles source maps, thresholds, reporters automatically | | Database seeding | Manual INSERT statements | drizzle-seed | Generates consistent, seeded random data with schema awareness | | Component mounting | Manual DOM manipulation | vitest-browser-svelte render() | Handles Svelte 5 lifecycle, context, and cleanup | | Screenshot on failure | Custom error handlers | Playwright built-in `screenshot: 'only-on-failure'` | Integrated with test lifecycle and artifacts | | CI test output parsing | Regex parsing | svelte-check --output machine | Structured, timestamp-prefixed output designed for CI | **Key insight:** The testing ecosystem has mature solutions for all common needs. Hand-rolling any of these leads to edge cases around cleanup, async timing, and framework integration that the official tools have already solved. ## Common Pitfalls ### Pitfall 1: jsdom Limitations with Svelte 5 Runes **What goes wrong:** Tests pass locally but fail to detect reactivity issues, or throw cryptic errors about $state **Why it happens:** jsdom simulates browser APIs but doesn't actually run JavaScript in a browser context. Svelte 5 runes compile differently and expect real browser reactivity. **How to avoid:** Use Vitest browser mode with Playwright provider for all `.svelte` component tests **Warning signs:** Tests involving $state, $derived, or $effect behave inconsistently or require excessive `await tick()` ### Pitfall 2: Missing flushSync for External State **What goes wrong:** Assertions fail because DOM hasn't updated after state change **Why it happens:** Svelte batches updates. When state changes outside component (e.g., store update in test), DOM update is async. **How to avoid:** Call `flushSync()` from 'svelte' after modifying external state before asserting **Warning signs:** Tests that work with longer timeouts but fail with short ones ### Pitfall 3: Parallel E2E with Shared Database **What goes wrong:** Flaky tests that sometimes pass, sometimes fail with data conflicts **Why it happens:** Multiple test workers modify the same database simultaneously **How to avoid:** Set `workers: 1` in playwright.config.ts for E2E tests. Use separate database per worker if parallelism is needed. **Warning signs:** Tests pass individually but fail in full suite runs ### Pitfall 4: Coverage Threshold Breaking Existing Code **What goes wrong:** CI fails immediately after enabling 80% threshold because existing code has 0% coverage **Why it happens:** Enabling coverage thresholds on existing codebase without tests **How to avoid:** Start with `thresholds: { autoUpdate: true }` to establish baseline, then incrementally raise thresholds as tests are added **Warning signs:** Immediate CI failure when coverage is first enabled ### Pitfall 5: SvelteKit Module Import Errors **What goes wrong:** Tests fail with "Cannot find module '$app/stores'" or similar **Why it happens:** $app/* modules are virtual modules provided by SvelteKit at build time, not available in test environment **How to avoid:** Mock all $app/* imports in vitest setup file. Keep mocks simple (don't use importOriginal with SvelteKit modules - causes SSR issues). **Warning signs:** Import errors mentioning $app, $env, or other SvelteKit virtual modules ### Pitfall 6: Playwright Browsers Not Installed in CI **What goes wrong:** CI fails with "browserType.launch: Executable doesn't exist" **Why it happens:** Playwright browsers need explicit installation, not included in npm install **How to avoid:** Add `npx playwright install --with-deps chromium` step before tests **Warning signs:** Works locally (where browsers are cached), fails in fresh CI environment ## Code Examples Verified patterns from official sources: ### vitest-setup-client.ts ```typescript // Source: https://vitest.dev/guide/browser/ /// /// ``` ### Package.json Scripts ```json { "scripts": { "test": "vitest", "test:unit": "vitest run", "test:unit:watch": "vitest", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" } } ``` ### CI Workflow (Gitea Actions) ```yaml # Source: https://docs.gitea.com/usage/actions/quickstart name: Test and Build on: push: branches: [master, main] pull_request: branches: [master, main] env: REGISTRY: git.kube2.tricnet.de IMAGE_NAME: admin/taskplaner jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Run type check run: npm run check -- --output machine - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Run unit tests with coverage run: npm run test:coverage - name: Run E2E tests run: npm run test:e2e env: CI: true - name: Upload test artifacts uses: actions/upload-artifact@v4 if: always() with: name: test-results path: | coverage/ playwright-report/ test-results/ build: needs: test runs-on: ubuntu-latest if: github.event_name != 'pull_request' || github.event.pull_request.merged == true steps: # ... existing build steps ... notify: needs: [test, build] runs-on: ubuntu-latest if: failure() steps: - name: Notify Slack on failure env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -X POST -H 'Content-type: application/json' \ --data '{"text":"Pipeline failed for ${{ gitea.repository }} on ${{ gitea.ref }}"}' \ $SLACK_WEBHOOK_URL ``` ### Playwright Config for E2E with Screenshots ```typescript // playwright.config.ts // Source: https://playwright.dev/docs/test-configuration import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', fullyParallel: false, // Sequential for shared database forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: 1, // Single worker for database tests reporter: [ ['html', { open: 'never' }], ['github'], // GitHub/Gitea compatible annotations ], use: { baseURL: process.env.BASE_URL || 'http://localhost:5173', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'off', // Per user decision: screenshots only, no video }, projects: [ { name: 'chromium-desktop', use: { ...devices['Desktop Chrome'] }, }, { name: 'chromium-mobile', use: { ...devices['Pixel 5'] }, }, ], webServer: { command: 'npm run build && npm run preview', port: 4173, reuseExistingServer: !process.env.CI, }, }); ``` ### svelte-check CI Output Format ```bash # Machine-readable output for CI parsing # Source: https://svelte.dev/docs/cli/sv-check npx svelte-check --output machine --tsconfig ./tsconfig.json # Output format: # 1590680326283 ERROR "/path/file.svelte" 10:5 "Type error message" # 1590680326807 COMPLETED 50 FILES 2 ERRORS 0 WARNINGS ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | jsdom + @testing-library/svelte | Vitest browser mode + vitest-browser-svelte | 2025 | Real browser testing, runes support | | Istanbul coverage | V8 coverage with AST remapping | Vitest 3.2 | 10x faster, same accuracy | | @playwright/experimental-ct-svelte | vitest-browser-svelte | 2025 | Better integration, official support | | Jest with svelte-jester | Vitest | 2024 | Native Vite support, faster | **Deprecated/outdated:** - `vitest-svelte-kit` package: Deprecated, no longer needed with modern Vitest - `@playwright/experimental-ct-svelte`: Use vitest-browser-svelte for component tests instead - `jsdom` for Svelte 5 components: Does not properly support runes reactivity ## Open Questions Things that couldn't be fully resolved: 1. **Exact drizzle-seed API for this schema** - What we know: drizzle-seed works with Drizzle ORM schemas - What's unclear: Specific configuration for the project's schema structure - Recommendation: Review drizzle-seed docs during implementation with actual schema 2. **Gitea Actions Slack notification action availability** - What we know: GitHub Actions Slack actions exist (rtCamp/action-slack-notify, etc.) - What's unclear: Whether these work identically in Gitea Actions - Recommendation: Use direct curl to Slack webhook (shown in examples) for maximum compatibility 3. **Vitest browser mode stability** - What we know: Vitest documents browser mode as "experimental" with stable core - What's unclear: Edge cases in production CI environments - Recommendation: Pin Vitest version, monitor for issues ## Sources ### Primary (HIGH confidence) - [Svelte Official Testing Docs](https://svelte.dev/docs/svelte/testing) - Official Vitest + browser mode recommendations - [Vitest Guide](https://vitest.dev/guide/) - Installation, configuration, browser mode - [Vitest Coverage Config](https://vitest.dev/config/coverage) - Threshold configuration - [Vitest Browser Mode](https://vitest.dev/guide/browser/) - Playwright provider setup - [svelte-check CLI](https://svelte.dev/docs/cli/sv-check) - CI output formats - [Gitea Actions Quickstart](https://docs.gitea.com/usage/actions/quickstart) - Workflow syntax ### Secondary (MEDIUM confidence) - [Scott Spence - Vitest Browser Mode Guide](https://scottspence.com/posts/testing-with-vitest-browser-svelte-guide) - Multi-project configuration - [Mainmatter - Database Fixtures](https://mainmatter.com/blog/2025/08/21/mock-database-in-svelte-tests/) - Drizzle seed pattern - [Roy Bakker - Playwright CI Guide](https://www.roybakker.dev/blog/playwright-in-ci-with-github-actions-and-docker-endtoend-guide) - Artifact upload, caching - [@testing-library/svelte Setup](https://testing-library.com/docs/svelte-testing-library/setup/) - Alternative jsdom approach ### Tertiary (LOW confidence) - Slack webhook notification patterns from various blog posts - curl approach is safest ## Metadata **Confidence breakdown:** - Standard stack: HIGH - Official Svelte docs explicitly recommend Vitest + browser mode - Architecture: HIGH - Multi-project pattern documented in Vitest and community guides - Pitfalls: HIGH - Common issues well-documented in GitHub issues and guides - E2E fixtures: MEDIUM - Drizzle-seed pattern documented but specific schema integration untested **Research date:** 2026-02-03 **Valid until:** 2026-03-03 (Vitest browser mode evolving, re-verify before major updates)