From 95f7d53c79325ce1c87ffe3ffca28cf02159da4e Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 21 Dec 2025 23:04:24 +0100 Subject: [PATCH 01/32] frontend unit+e2e tests --- .github/workflows/frontend-tests.yml | 196 ++ README.md | 5 +- frontend/e2e/auth.spec.ts | 124 + frontend/e2e/theme.spec.ts | 75 + frontend/package-lock.json | 2649 +++++++++++++++-- frontend/package.json | 16 +- frontend/playwright.config.ts | 29 + frontend/src/stores/__tests__/auth.test.ts | 354 +++ .../__tests__/notificationStore.test.ts | 461 +++ frontend/src/stores/__tests__/theme.test.ts | 227 ++ .../src/stores/__tests__/toastStore.test.ts | 162 + frontend/vitest.config.ts | 34 + frontend/vitest.setup.ts | 75 + 13 files changed, 4168 insertions(+), 239 deletions(-) create mode 100644 .github/workflows/frontend-tests.yml create mode 100644 frontend/e2e/auth.spec.ts create mode 100644 frontend/e2e/theme.spec.ts create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/src/stores/__tests__/auth.test.ts create mode 100644 frontend/src/stores/__tests__/notificationStore.test.ts create mode 100644 frontend/src/stores/__tests__/theme.test.ts create mode 100644 frontend/src/stores/__tests__/toastStore.test.ts create mode 100644 frontend/vitest.config.ts create mode 100644 frontend/vitest.setup.ts diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml new file mode 100644 index 00000000..d41859fc --- /dev/null +++ b/.github/workflows/frontend-tests.yml @@ -0,0 +1,196 @@ +name: Frontend Tests + +on: + push: + branches: [main, dev] + paths: + - 'frontend/**' + - '.github/workflows/frontend-tests.yml' + pull_request: + branches: [main, dev] + paths: + - 'frontend/**' + - '.github/workflows/frontend-tests.yml' + workflow_dispatch: + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test + + - name: Run tests with coverage + run: npm run test:coverage + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: frontend-coverage + path: frontend/coverage/ + + e2e-tests: + name: E2E Tests + runs-on: ubuntu-latest + needs: unit-tests + steps: + - uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install yq + run: | + sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq + sudo chmod +x /usr/local/bin/yq + + - name: Setup Kubernetes (k3s) + run: | + curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --tls-san host.docker.internal" sh - + mkdir -p $HOME/.kube + sudo k3s kubectl config view --raw > $HOME/.kube/config + sudo chmod 600 $HOME/.kube/config + timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done' + + - name: Create kubeconfig for CI + run: | + cat > backend/kubeconfig.yaml < logs/docker-compose.log + docker compose -f docker-compose.ci.yaml logs frontend > logs/frontend.log + docker compose -f docker-compose.ci.yaml logs backend > logs/backend.log + + - name: Upload logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-logs + path: logs/ diff --git a/README.md b/README.md index 7d63ce51..30ddba6e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,10 @@ Docker Scan Status - Tests Status + Backend Tests Status + + + Frontend Tests Status Backend Coverage diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts new file mode 100644 index 00000000..a38b96c3 --- /dev/null +++ b/frontend/e2e/auth.spec.ts @@ -0,0 +1,124 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Authentication', () => { + test.beforeEach(async ({ page }) => { + // Clear any existing auth state + await page.context().clearCookies(); + }); + + test('shows login page with form elements', async ({ page }) => { + await page.goto('/login'); + + await expect(page.locator('h2')).toContainText('Sign in to your account'); + await expect(page.locator('#username')).toBeVisible(); + await expect(page.locator('#password')).toBeVisible(); + await expect(page.locator('button[type="submit"]')).toBeVisible(); + }); + + test('shows validation when submitting empty form', async ({ page }) => { + await page.goto('/login'); + + // HTML5 validation should prevent submission + const usernameInput = page.locator('#username'); + await expect(usernameInput).toHaveAttribute('required', ''); + }); + + test('shows error with invalid credentials', async ({ page }) => { + await page.goto('/login'); + + await page.fill('#username', 'invaliduser'); + await page.fill('#password', 'wrongpassword'); + await page.click('button[type="submit"]'); + + // Wait for error message to appear + await expect(page.locator('p.text-red-600, p.text-red-400')).toBeVisible({ timeout: 10000 }); + }); + + test('redirects to editor on successful login', async ({ page }) => { + await page.goto('/login'); + + // Use test credentials (adjust based on your test environment) + await page.fill('#username', 'user'); + await page.fill('#password', 'user123'); + await page.click('button[type="submit"]'); + + // Should redirect to editor + await expect(page).toHaveURL(/\/editor/, { timeout: 15000 }); + }); + + test('shows loading state during login', async ({ page }) => { + await page.goto('/login'); + + await page.fill('#username', 'user'); + await page.fill('#password', 'user123'); + + // Start login but don't wait for it + const submitButton = page.locator('button[type="submit"]'); + await submitButton.click(); + + // Button should show loading text + await expect(submitButton).toContainText(/Logging in|Sign in/); + }); + + test('redirects unauthenticated users from protected routes', async ({ page }) => { + // Try to access protected route + await page.goto('/editor'); + + // Should redirect to login + await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); + }); + + test('preserves redirect path after login', async ({ page }) => { + // Try to access specific protected route + await page.goto('/settings'); + + // Should redirect to login + await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); + + // Login + await page.fill('#username', 'user'); + await page.fill('#password', 'user123'); + await page.click('button[type="submit"]'); + + // Should redirect back to settings + await expect(page).toHaveURL(/\/settings/, { timeout: 15000 }); + }); + + test('has link to registration page', async ({ page }) => { + await page.goto('/login'); + + const registerLink = page.locator('a[href="/register"]'); + await expect(registerLink).toBeVisible(); + await expect(registerLink).toContainText('create a new account'); + }); + + test('can navigate to registration page', async ({ page }) => { + await page.goto('/login'); + + await page.click('a[href="/register"]'); + + await expect(page).toHaveURL(/\/register/); + }); +}); + +test.describe('Logout', () => { + test.beforeEach(async ({ page }) => { + // Login first + await page.goto('/login'); + await page.fill('#username', 'user'); + await page.fill('#password', 'user123'); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL(/\/editor/, { timeout: 15000 }); + }); + + test('can logout from authenticated state', async ({ page }) => { + // Find and click logout button (adjust selector based on your UI) + const logoutButton = page.locator('button:has-text("Logout"), a:has-text("Logout"), [data-testid="logout"]'); + + if (await logoutButton.isVisible()) { + await logoutButton.click(); + // Should redirect to login + await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); + } + }); +}); diff --git a/frontend/e2e/theme.spec.ts b/frontend/e2e/theme.spec.ts new file mode 100644 index 00000000..f71a50a9 --- /dev/null +++ b/frontend/e2e/theme.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Theme', () => { + test.beforeEach(async ({ page }) => { + // Clear localStorage to start fresh + await page.goto('/login'); + await page.evaluate(() => localStorage.removeItem('app-theme')); + }); + + test('defaults to auto theme (no dark class)', async ({ page }) => { + await page.goto('/login'); + + // Check that dark class is not present initially (assuming system prefers light) + const hasDarkClass = await page.evaluate(() => + document.documentElement.classList.contains('dark') + ); + // This may be true or false depending on system preference + expect(typeof hasDarkClass).toBe('boolean'); + }); + + test('persists theme preference in localStorage', async ({ page }) => { + await page.goto('/login'); + + // Set theme via localStorage + await page.evaluate(() => localStorage.setItem('app-theme', 'dark')); + await page.reload(); + + const storedTheme = await page.evaluate(() => localStorage.getItem('app-theme')); + expect(storedTheme).toBe('dark'); + }); + + test('applies dark theme when set', async ({ page }) => { + await page.goto('/login'); + + // Set dark theme + await page.evaluate(() => { + localStorage.setItem('app-theme', 'dark'); + document.documentElement.classList.add('dark'); + }); + + const hasDarkClass = await page.evaluate(() => + document.documentElement.classList.contains('dark') + ); + expect(hasDarkClass).toBe(true); + }); + + test('applies light theme when set', async ({ page }) => { + await page.goto('/login'); + + // Set light theme + await page.evaluate(() => { + localStorage.setItem('app-theme', 'light'); + document.documentElement.classList.remove('dark'); + }); + + const hasDarkClass = await page.evaluate(() => + document.documentElement.classList.contains('dark') + ); + expect(hasDarkClass).toBe(false); + }); + + test('theme persists across page navigation', async ({ page }) => { + await page.goto('/login'); + + // Set dark theme + await page.evaluate(() => localStorage.setItem('app-theme', 'dark')); + await page.reload(); + + // Navigate to another page + await page.goto('/register'); + + const storedTheme = await page.evaluate(() => localStorage.getItem('app-theme')); + expect(storedTheme).toBe('dark'); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ea1d447e..a7150b51 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -44,17 +44,30 @@ "devDependencies": { "@babel/runtime": "^7.24.7", "@hey-api/openapi-ts": "0.89.1", + "@playwright/test": "^1.52.0", "@rollup/plugin-typescript": "^12.1.2", + "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/forms": "^0.5.11", "@tailwindcss/postcss": "^4.1.13", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/svelte": "^5.2.6", + "@testing-library/user-event": "^14.6.1", "express": "^5.2.1", "http-proxy": "^1.18.1", + "jsdom": "^26.1.0", "rollup-plugin-serve": "^1.1.1", "tailwindcss": "^4.1.13", "tslib": "^2.8.1", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "vitest": "^3.2.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -67,6 +80,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -173,165 +222,691 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@hey-api/codegen-core": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.4.0.tgz", - "integrity": "sha512-o8rBbEXEUhEPzrHbqImYjwIHm4Oj0r1RPS+5cp8Z66kPO7SEN7PYUgK7XpmSxoy9LPMNK1M5qmCO4cGGwT+ELQ==", + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, - "dependencies": { - "ansi-colors": "4.1.3", - "color-support": "1.1.3" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" }, "peerDependencies": { - "typescript": ">=5.5.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@hey-api/json-schema-ref-parser": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.2.2.tgz", - "integrity": "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA==", + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.1", - "lodash": "^4.17.21" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { - "node": ">= 16" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/hey-api" + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@hey-api/openapi-ts": { - "version": "0.89.1", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.89.1.tgz", - "integrity": "sha512-1iG8e0hLIiaImFJdqXBNh9yu5B6oYUicrS/x/MwyWGuGH1A2D8DSjMKHbIfU6PIg4HH0rjZlRt2FoxDlBEGMRg==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, - "dependencies": { - "@hey-api/codegen-core": "^0.4.0", - "@hey-api/json-schema-ref-parser": "1.2.2", - "ansi-colors": "4.1.3", - "c12": "3.3.2", - "color-support": "1.1.3", - "commander": "14.0.2", - "open": "11.0.0", - "semver": "7.7.3" - }, - "bin": { - "openapi-ts": "bin/run.js" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" + "node": ">=18" }, "peerDependencies": { - "typescript": ">=5.5.3" + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@hey-api/openapi-ts/node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "engines": { - "node": ">=20" + "node": ">=18" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "minipass": "^7.0.4" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true - }, - "node_modules/@lezer/common": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz", - "integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", - "dependencies": { - "@lezer/common": "^1.3.0" + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@lezer/lr": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz", - "integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==", - "dependencies": { + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hey-api/codegen-core": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.4.0.tgz", + "integrity": "sha512-o8rBbEXEUhEPzrHbqImYjwIHm4Oj0r1RPS+5cp8Z66kPO7SEN7PYUgK7XpmSxoy9LPMNK1M5qmCO4cGGwT+ELQ==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.3", + "color-support": "1.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.2.2.tgz", + "integrity": "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.89.1", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.89.1.tgz", + "integrity": "sha512-1iG8e0hLIiaImFJdqXBNh9yu5B6oYUicrS/x/MwyWGuGH1A2D8DSjMKHbIfU6PIg4HH0rjZlRt2FoxDlBEGMRg==", + "dev": true, + "dependencies": { + "@hey-api/codegen-core": "^0.4.0", + "@hey-api/json-schema-ref-parser": "1.2.2", + "ansi-colors": "4.1.3", + "c12": "3.3.2", + "color-support": "1.1.3", + "commander": "14.0.2", + "open": "11.0.0", + "semver": "7.7.3" + }, + "bin": { + "openapi-ts": "bin/run.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, + "node_modules/@lezer/common": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz", + "integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz", + "integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==", + "dependencies": { "@lezer/common": "^1.0.0" } }, @@ -358,28 +933,105 @@ "svelte": "^5.0.0" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==" - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", - "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==", + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", + "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", "dependencies": { "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^8.0.3", - "is-reference": "1.2.1", - "magic-string": "^0.27.0" + "magic-string": "^0.30.3" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.68.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -387,18 +1039,28 @@ } } }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "node_modules/@rollup/plugin-replace/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dependencies": { - "@rollup/pluginutils": "^5.1.0" + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -406,22 +1068,46 @@ } } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "node_modules/@rollup/plugin-typescript": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", + "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", + "dev": true, "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", + "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -429,108 +1115,345 @@ } } }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", - "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@rollup/plugin-replace/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=14.0.0" - }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "acorn": "^8.9.0" } }, - "node_modules/@rollup/plugin-typescript": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", - "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "resolve": "^1.22.1" + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" }, "engines": { - "node": ">=14.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "rollup": "^2.14.0||^3.0.0||^4.0.0", - "tslib": "*", - "typescript": ">=3.7.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "tslib": { - "optional": true - } + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" + "debug": "^4.3.7" }, "engines": { - "node": ">=14.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", - "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", - "peerDependencies": { - "acorn": "^8.9.0" + "node_modules/@sveltejs/vite-plugin-svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/@tailwindcss/forms": { @@ -815,6 +1738,97 @@ "tailwindcss": "4.1.13" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/svelte": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.10.tgz", + "integrity": "sha512-siiR6FknvRN0Tt4m8mf0ejvahSRi3/n10Awyns0R7huMCNBHSrSzzXa//hJqhtEstZ7b2ZZMZwuYhcD0BIk/bA==", + "dev": true, + "dependencies": { + "@testing-library/dom": "9.x.x || 10.x.x" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", + "vite": "*", + "vitest": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -823,6 +1837,28 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -918,6 +1954,141 @@ "@codemirror/view": ">=6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -942,6 +2113,15 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -951,6 +2131,15 @@ "node": ">=6" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1016,6 +2205,15 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -1213,6 +2411,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1272,6 +2479,22 @@ } ] }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1287,6 +2510,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1506,6 +2738,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1590,6 +2828,14 @@ "postcss": "^8.2.15" } }, + "node_modules/cssnano/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -1618,6 +2864,32 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1635,6 +2907,21 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1698,6 +2985,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -1718,6 +3014,12 @@ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -1861,6 +3163,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1873,6 +3181,47 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1919,6 +3268,15 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -1968,6 +3326,23 @@ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "dev": true }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2216,6 +3591,18 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2250,6 +3637,32 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", @@ -2304,6 +3717,15 @@ "node": ">=8" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2430,6 +3852,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -2468,6 +3896,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2480,6 +3914,66 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2788,6 +4282,27 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", @@ -2866,6 +4381,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -2996,6 +4520,12 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true + }, "node_modules/nypm": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", @@ -3121,6 +4651,30 @@ "node": ">=8" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3151,6 +4705,15 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", @@ -3195,6 +4758,50 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3338,6 +4945,14 @@ } } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-merge-longhand": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", @@ -3754,6 +5369,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/promise.series": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", @@ -3775,6 +5416,15 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -3832,6 +5482,12 @@ "destr": "^2.0.3" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3854,6 +5510,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -4039,6 +5708,12 @@ "node": ">= 18" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -4092,6 +5767,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semiver": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", @@ -4243,6 +5930,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -4313,6 +6006,12 @@ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4322,11 +6021,47 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==" }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, "node_modules/style-inject": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", @@ -4506,6 +6241,12 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/tailwindcss": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", @@ -4563,6 +6304,12 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, "node_modules/tinydate": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", @@ -4580,6 +6327,67 @@ "node": ">=18" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4608,6 +6416,30 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4693,11 +6525,332 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4739,6 +6892,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -4749,11 +6917,20 @@ } }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/zimmerframe": { diff --git a/frontend/package.json b/frontend/package.json index f73adc4d..d3915732 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,12 @@ "build": "npx rollup -c", "dev": "npx rollup -c -w", "start": "sirv public --single --no-clear --dev --host", - "generate:api": "openapi-ts" + "generate:api": "openapi-ts", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test" }, "dependencies": { "@babel/runtime": "^7.27.6", @@ -45,15 +50,22 @@ }, "devDependencies": { "@babel/runtime": "^7.24.7", + "@playwright/test": "^1.52.0", "@rollup/plugin-typescript": "^12.1.2", + "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/forms": "^0.5.11", "@tailwindcss/postcss": "^4.1.13", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/svelte": "^5.2.6", + "@testing-library/user-event": "^14.6.1", "express": "^5.2.1", "http-proxy": "^1.18.1", "@hey-api/openapi-ts": "0.89.1", + "jsdom": "^26.1.0", "rollup-plugin-serve": "^1.1.1", "tailwindcss": "^4.1.13", "tslib": "^2.8.1", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "vitest": "^3.2.4" } } diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 00000000..719b310b --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'https://localhost:5001', + ignoreHTTPSErrors: true, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npm run dev', + url: 'https://localhost:5001', + reuseExistingServer: !process.env.CI, + ignoreHTTPSErrors: true, + timeout: 120000, + }, +}); diff --git a/frontend/src/stores/__tests__/auth.test.ts b/frontend/src/stores/__tests__/auth.test.ts new file mode 100644 index 00000000..eb40b3bd --- /dev/null +++ b/frontend/src/stores/__tests__/auth.test.ts @@ -0,0 +1,354 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { get } from 'svelte/store'; + +// Mock the API functions +const mockLoginApi = vi.fn(); +const mockLogoutApi = vi.fn(); +const mockVerifyTokenApi = vi.fn(); +const mockGetProfileApi = vi.fn(); + +vi.mock('../../lib/api', () => ({ + loginApiV1AuthLoginPost: (...args: unknown[]) => mockLoginApi(...args), + logoutApiV1AuthLogoutPost: (...args: unknown[]) => mockLogoutApi(...args), + verifyTokenApiV1AuthVerifyTokenGet: (...args: unknown[]) => mockVerifyTokenApi(...args), + getCurrentUserProfileApiV1AuthMeGet: (...args: unknown[]) => mockGetProfileApi(...args), +})); + +describe('auth store', () => { + let localStorageData: Record = {}; + + beforeEach(async () => { + // Reset localStorage mock + localStorageData = {}; + vi.mocked(localStorage.getItem).mockImplementation((key: string) => localStorageData[key] ?? null); + vi.mocked(localStorage.setItem).mockImplementation((key: string, value: string) => { + localStorageData[key] = value; + }); + vi.mocked(localStorage.removeItem).mockImplementation((key: string) => { + delete localStorageData[key]; + }); + + // Reset all mocks + mockLoginApi.mockReset(); + mockLogoutApi.mockReset(); + mockVerifyTokenApi.mockReset(); + mockGetProfileApi.mockReset(); + + // Clear module cache to reset store state + vi.resetModules(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('initial state', () => { + it('has null authentication state initially', async () => { + const { isAuthenticated } = await import('../auth'); + expect(get(isAuthenticated)).toBe(null); + }); + + it('has null username initially', async () => { + const { username } = await import('../auth'); + expect(get(username)).toBe(null); + }); + + it('has null userRole initially', async () => { + const { userRole } = await import('../auth'); + expect(get(userRole)).toBe(null); + }); + + it('restores auth state from localStorage', async () => { + const authState = { + isAuthenticated: true, + username: 'testuser', + userRole: 'user', + csrfToken: 'test-token', + userId: 'user-123', + userEmail: 'test@example.com', + timestamp: Date.now(), + }; + localStorageData['authState'] = JSON.stringify(authState); + + const { isAuthenticated, username, userRole, csrfToken } = await import('../auth'); + + expect(get(isAuthenticated)).toBe(true); + expect(get(username)).toBe('testuser'); + expect(get(userRole)).toBe('user'); + expect(get(csrfToken)).toBe('test-token'); + }); + + it('clears expired auth state from localStorage', async () => { + const authState = { + isAuthenticated: true, + username: 'testuser', + userRole: 'user', + csrfToken: 'test-token', + userId: 'user-123', + userEmail: 'test@example.com', + timestamp: Date.now() - 25 * 60 * 60 * 1000, // 25 hours ago + }; + localStorageData['authState'] = JSON.stringify(authState); + + const { isAuthenticated } = await import('../auth'); + + expect(get(isAuthenticated)).toBe(null); + expect(localStorage.removeItem).toHaveBeenCalledWith('authState'); + }); + }); + + describe('login', () => { + it('sets auth state on successful login', async () => { + mockLoginApi.mockResolvedValue({ + data: { + username: 'testuser', + role: 'user', + csrf_token: 'new-csrf-token', + }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + + const { login, isAuthenticated, username, userRole, csrfToken } = await import('../auth'); + + const result = await login('testuser', 'password123'); + + expect(result).toBe(true); + expect(get(isAuthenticated)).toBe(true); + expect(get(username)).toBe('testuser'); + expect(get(userRole)).toBe('user'); + expect(get(csrfToken)).toBe('new-csrf-token'); + }); + + it('persists auth state to localStorage on login', async () => { + mockLoginApi.mockResolvedValue({ + data: { + username: 'testuser', + role: 'user', + csrf_token: 'csrf-token', + }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + + const { login } = await import('../auth'); + await login('testuser', 'password123'); + + expect(localStorage.setItem).toHaveBeenCalledWith( + 'authState', + expect.stringContaining('testuser') + ); + }); + + it('throws error on failed login', async () => { + mockLoginApi.mockResolvedValue({ + data: null, + error: { detail: 'Invalid credentials' }, + }); + + const { login } = await import('../auth'); + + await expect(login('baduser', 'badpass')).rejects.toBeDefined(); + }); + + it('calls API with correct parameters', async () => { + mockLoginApi.mockResolvedValue({ + data: { username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + + const { login } = await import('../auth'); + await login('testuser', 'mypassword'); + + expect(mockLoginApi).toHaveBeenCalledWith({ + body: { username: 'testuser', password: 'mypassword', scope: '' }, + }); + }); + }); + + describe('logout', () => { + it('clears auth state on logout', async () => { + // Set up authenticated state + mockLoginApi.mockResolvedValue({ + data: { username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + mockLogoutApi.mockResolvedValue({ data: {}, error: null }); + + const { login, logout, isAuthenticated, username } = await import('../auth'); + + await login('testuser', 'password'); + expect(get(isAuthenticated)).toBe(true); + + await logout(); + + expect(get(isAuthenticated)).toBe(false); + expect(get(username)).toBe(null); + }); + + it('clears localStorage on logout', async () => { + mockLogoutApi.mockResolvedValue({ data: {}, error: null }); + + const { logout } = await import('../auth'); + await logout(); + + expect(localStorage.removeItem).toHaveBeenCalledWith('authState'); + }); + + it('still clears state even if API call fails', async () => { + mockLoginApi.mockResolvedValue({ + data: { username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + mockLogoutApi.mockRejectedValue(new Error('Network error')); + + const { login, logout, isAuthenticated } = await import('../auth'); + + await login('testuser', 'password'); + await logout(); + + expect(get(isAuthenticated)).toBe(false); + }); + }); + + describe('verifyAuth', () => { + it('returns true when verification succeeds', async () => { + mockVerifyTokenApi.mockResolvedValue({ + data: { valid: true, username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + + const { verifyAuth } = await import('../auth'); + const result = await verifyAuth(true); + + expect(result).toBe(true); + }); + + it('returns false and clears state when verification fails', async () => { + mockVerifyTokenApi.mockResolvedValue({ + data: { valid: false }, + error: null, + }); + + const { verifyAuth, isAuthenticated } = await import('../auth'); + const result = await verifyAuth(true); + + expect(result).toBe(false); + expect(get(isAuthenticated)).toBe(false); + }); + + it('uses cache when not force refreshing', async () => { + mockVerifyTokenApi.mockResolvedValue({ + data: { valid: true, username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + + const { verifyAuth } = await import('../auth'); + + // First call - should hit API + await verifyAuth(true); + expect(mockVerifyTokenApi).toHaveBeenCalledTimes(1); + + // Second call without force - should use cache + await verifyAuth(false); + expect(mockVerifyTokenApi).toHaveBeenCalledTimes(1); + }); + + it('bypasses cache when force refreshing', async () => { + mockVerifyTokenApi.mockResolvedValue({ + data: { valid: true, username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + + const { verifyAuth } = await import('../auth'); + + await verifyAuth(true); + await verifyAuth(true); + + expect(mockVerifyTokenApi).toHaveBeenCalledTimes(2); + }); + + it('returns cached value on network error (offline-first)', async () => { + // First successful verification + mockVerifyTokenApi.mockResolvedValueOnce({ + data: { valid: true, username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-123', email: 'test@example.com' }, + error: null, + }); + + const { verifyAuth } = await import('../auth'); + + const firstResult = await verifyAuth(true); + expect(firstResult).toBe(true); + + // Second call with network error + mockVerifyTokenApi.mockRejectedValueOnce(new Error('Network error')); + + const secondResult = await verifyAuth(true); + expect(secondResult).toBe(true); // Should return cached value + }); + }); + + describe('fetchUserProfile', () => { + it('updates userId and userEmail on success', async () => { + mockLoginApi.mockResolvedValue({ + data: { username: 'testuser', role: 'user', csrf_token: 'token' }, + error: null, + }); + mockGetProfileApi.mockResolvedValue({ + data: { user_id: 'user-456', email: 'newemail@example.com' }, + error: null, + }); + + const { login, userId, userEmail } = await import('../auth'); + await login('testuser', 'password'); + + expect(get(userId)).toBe('user-456'); + expect(get(userEmail)).toBe('newemail@example.com'); + }); + + it('throws error on failed profile fetch', async () => { + mockGetProfileApi.mockResolvedValue({ + data: null, + error: { detail: 'Unauthorized' }, + }); + + const { fetchUserProfile } = await import('../auth'); + + await expect(fetchUserProfile()).rejects.toBeDefined(); + }); + }); +}); diff --git a/frontend/src/stores/__tests__/notificationStore.test.ts b/frontend/src/stores/__tests__/notificationStore.test.ts new file mode 100644 index 00000000..81a2bfe1 --- /dev/null +++ b/frontend/src/stores/__tests__/notificationStore.test.ts @@ -0,0 +1,461 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { get } from 'svelte/store'; + +// Mock the API functions +const mockGetNotifications = vi.fn(); +const mockMarkRead = vi.fn(); +const mockMarkAllRead = vi.fn(); +const mockDeleteNotification = vi.fn(); + +vi.mock('../../lib/api', () => ({ + getNotificationsApiV1NotificationsGet: (...args: unknown[]) => mockGetNotifications(...args), + markNotificationReadApiV1NotificationsNotificationIdReadPut: (...args: unknown[]) => mockMarkRead(...args), + markAllReadApiV1NotificationsMarkAllReadPost: (...args: unknown[]) => mockMarkAllRead(...args), + deleteNotificationApiV1NotificationsNotificationIdDelete: (...args: unknown[]) => mockDeleteNotification(...args), +})); + +const createMockNotification = (overrides = {}) => ({ + notification_id: `notif-${Math.random().toString(36).slice(2)}`, + title: 'Test Notification', + message: 'Test message', + status: 'unread' as const, + created_at: new Date().toISOString(), + tags: [], + ...overrides, +}); + +describe('notificationStore', () => { + beforeEach(async () => { + mockGetNotifications.mockReset(); + mockMarkRead.mockReset(); + mockMarkAllRead.mockReset(); + mockDeleteNotification.mockReset(); + vi.resetModules(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('initial state', () => { + it('has empty notifications array', async () => { + const { notificationStore } = await import('../notificationStore'); + expect(get(notificationStore).notifications).toEqual([]); + }); + + it('has loading false', async () => { + const { notificationStore } = await import('../notificationStore'); + expect(get(notificationStore).loading).toBe(false); + }); + + it('has null error', async () => { + const { notificationStore } = await import('../notificationStore'); + expect(get(notificationStore).error).toBe(null); + }); + }); + + describe('load', () => { + it('sets loading true during fetch', async () => { + let capturedLoading = false; + mockGetNotifications.mockImplementation(() => { + return new Promise((resolve) => { + setTimeout(() => resolve({ data: { notifications: [] }, error: null }), 10); + }); + }); + + const { notificationStore } = await import('../notificationStore'); + const loadPromise = notificationStore.load(); + + capturedLoading = get(notificationStore).loading; + await loadPromise; + + expect(capturedLoading).toBe(true); + }); + + it('populates notifications on success', async () => { + const notifications = [ + createMockNotification({ notification_id: 'n1', title: 'First' }), + createMockNotification({ notification_id: 'n2', title: 'Second' }), + ]; + mockGetNotifications.mockResolvedValue({ + data: { notifications }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + expect(get(notificationStore).notifications).toHaveLength(2); + expect(get(notificationStore).notifications[0].title).toBe('First'); + }); + + it('sets loading false after success', async () => { + mockGetNotifications.mockResolvedValue({ + data: { notifications: [] }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + expect(get(notificationStore).loading).toBe(false); + }); + + it('returns fetched notifications', async () => { + const notifications = [createMockNotification()]; + mockGetNotifications.mockResolvedValue({ + data: { notifications }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + const result = await notificationStore.load(); + + expect(result).toEqual(notifications); + }); + + it('sets error on failure', async () => { + mockGetNotifications.mockResolvedValue({ + data: null, + error: { detail: [{ msg: 'Failed to fetch' }] }, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + expect(get(notificationStore).error).toBe('Failed to fetch'); + }); + + it('returns empty array on failure', async () => { + mockGetNotifications.mockResolvedValue({ + data: null, + error: { detail: [{ msg: 'Error' }] }, + }); + + const { notificationStore } = await import('../notificationStore'); + const result = await notificationStore.load(); + + expect(result).toEqual([]); + }); + + it('passes limit to API', async () => { + mockGetNotifications.mockResolvedValue({ + data: { notifications: [] }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(50); + + expect(mockGetNotifications).toHaveBeenCalledWith({ + query: { limit: 50, include_tags: undefined, exclude_tags: undefined, tag_prefix: undefined }, + }); + }); + + it('passes tag filters to API', async () => { + mockGetNotifications.mockResolvedValue({ + data: { notifications: [] }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(20, { + include_tags: ['important'], + exclude_tags: ['spam'], + tag_prefix: 'system:', + }); + + expect(mockGetNotifications).toHaveBeenCalledWith({ + query: { + limit: 20, + include_tags: ['important'], + exclude_tags: ['spam'], + tag_prefix: 'system:', + }, + }); + }); + }); + + describe('add', () => { + it('adds notification to the beginning', async () => { + mockGetNotifications.mockResolvedValue({ + data: { notifications: [createMockNotification({ notification_id: 'existing' })] }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + const newNotification = createMockNotification({ notification_id: 'new', title: 'New' }); + notificationStore.add(newNotification); + + const notifications = get(notificationStore).notifications; + expect(notifications[0].notification_id).toBe('new'); + expect(notifications).toHaveLength(2); + }); + + it('caps notifications at 100', async () => { + const { notificationStore } = await import('../notificationStore'); + + // Add 100 notifications + for (let i = 0; i < 100; i++) { + notificationStore.add(createMockNotification({ notification_id: `n${i}` })); + } + + expect(get(notificationStore).notifications).toHaveLength(100); + + // Add one more + notificationStore.add(createMockNotification({ notification_id: 'new' })); + + const notifications = get(notificationStore).notifications; + expect(notifications).toHaveLength(100); + expect(notifications[0].notification_id).toBe('new'); + }); + }); + + describe('markAsRead', () => { + it('marks notification as read on success', async () => { + mockGetNotifications.mockResolvedValue({ + data: { + notifications: [ + createMockNotification({ notification_id: 'n1', status: 'unread' }), + ], + }, + error: null, + }); + mockMarkRead.mockResolvedValue({ error: null }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + const result = await notificationStore.markAsRead('n1'); + + expect(result).toBe(true); + expect(get(notificationStore).notifications[0].status).toBe('read'); + }); + + it('returns false on failure', async () => { + mockGetNotifications.mockResolvedValue({ + data: { notifications: [createMockNotification({ notification_id: 'n1' })] }, + error: null, + }); + mockMarkRead.mockResolvedValue({ error: { detail: 'Failed' } }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + const result = await notificationStore.markAsRead('n1'); + + expect(result).toBe(false); + expect(get(notificationStore).notifications[0].status).toBe('unread'); + }); + + it('calls API with correct notification ID', async () => { + mockMarkRead.mockResolvedValue({ error: null }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.markAsRead('test-id-123'); + + expect(mockMarkRead).toHaveBeenCalledWith({ + path: { notification_id: 'test-id-123' }, + }); + }); + }); + + describe('markAllAsRead', () => { + it('marks all notifications as read on success', async () => { + mockGetNotifications.mockResolvedValue({ + data: { + notifications: [ + createMockNotification({ notification_id: 'n1', status: 'unread' }), + createMockNotification({ notification_id: 'n2', status: 'unread' }), + ], + }, + error: null, + }); + mockMarkAllRead.mockResolvedValue({ error: null }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + const result = await notificationStore.markAllAsRead(); + + expect(result).toBe(true); + const notifications = get(notificationStore).notifications; + expect(notifications.every(n => n.status === 'read')).toBe(true); + }); + + it('returns false on failure', async () => { + mockMarkAllRead.mockResolvedValue({ error: { detail: 'Failed' } }); + + const { notificationStore } = await import('../notificationStore'); + const result = await notificationStore.markAllAsRead(); + + expect(result).toBe(false); + }); + }); + + describe('delete', () => { + it('removes notification on success', async () => { + mockGetNotifications.mockResolvedValue({ + data: { + notifications: [ + createMockNotification({ notification_id: 'n1' }), + createMockNotification({ notification_id: 'n2' }), + ], + }, + error: null, + }); + mockDeleteNotification.mockResolvedValue({ error: null }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + const result = await notificationStore.delete('n1'); + + expect(result).toBe(true); + const notifications = get(notificationStore).notifications; + expect(notifications).toHaveLength(1); + expect(notifications[0].notification_id).toBe('n2'); + }); + + it('returns false on failure', async () => { + mockGetNotifications.mockResolvedValue({ + data: { notifications: [createMockNotification({ notification_id: 'n1' })] }, + error: null, + }); + mockDeleteNotification.mockResolvedValue({ error: { detail: 'Failed' } }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + + const result = await notificationStore.delete('n1'); + + expect(result).toBe(false); + expect(get(notificationStore).notifications).toHaveLength(1); + }); + + it('calls API with correct notification ID', async () => { + mockDeleteNotification.mockResolvedValue({ error: null }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.delete('delete-me-123'); + + expect(mockDeleteNotification).toHaveBeenCalledWith({ + path: { notification_id: 'delete-me-123' }, + }); + }); + }); + + describe('clear', () => { + it('clears all notifications', async () => { + mockGetNotifications.mockResolvedValue({ + data: { + notifications: [ + createMockNotification(), + createMockNotification(), + ], + }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.load(); + expect(get(notificationStore).notifications).toHaveLength(2); + + notificationStore.clear(); + + expect(get(notificationStore).notifications).toEqual([]); + }); + }); + + describe('refresh', () => { + it('calls load with default limit', async () => { + mockGetNotifications.mockResolvedValue({ + data: { notifications: [] }, + error: null, + }); + + const { notificationStore } = await import('../notificationStore'); + await notificationStore.refresh(); + + expect(mockGetNotifications).toHaveBeenCalledWith({ + query: { limit: 20, include_tags: undefined, exclude_tags: undefined, tag_prefix: undefined }, + }); + }); + }); + + describe('unreadCount derived store', () => { + it('counts unread notifications', async () => { + mockGetNotifications.mockResolvedValue({ + data: { + notifications: [ + createMockNotification({ notification_id: 'n1', status: 'unread' }), + createMockNotification({ notification_id: 'n2', status: 'read' }), + createMockNotification({ notification_id: 'n3', status: 'unread' }), + ], + }, + error: null, + }); + + const { notificationStore, unreadCount } = await import('../notificationStore'); + await notificationStore.load(); + + expect(get(unreadCount)).toBe(2); + }); + + it('returns 0 when all are read', async () => { + mockGetNotifications.mockResolvedValue({ + data: { + notifications: [ + createMockNotification({ notification_id: 'n1', status: 'read' }), + ], + }, + error: null, + }); + + const { notificationStore, unreadCount } = await import('../notificationStore'); + await notificationStore.load(); + + expect(get(unreadCount)).toBe(0); + }); + + it('updates when notification is marked as read', async () => { + mockGetNotifications.mockResolvedValue({ + data: { + notifications: [ + createMockNotification({ notification_id: 'n1', status: 'unread' }), + ], + }, + error: null, + }); + mockMarkRead.mockResolvedValue({ error: null }); + + const { notificationStore, unreadCount } = await import('../notificationStore'); + await notificationStore.load(); + expect(get(unreadCount)).toBe(1); + + await notificationStore.markAsRead('n1'); + expect(get(unreadCount)).toBe(0); + }); + }); + + describe('notifications derived store', () => { + it('exposes notifications array', async () => { + const notifs = [ + createMockNotification({ notification_id: 'n1', title: 'First' }), + createMockNotification({ notification_id: 'n2', title: 'Second' }), + ]; + mockGetNotifications.mockResolvedValue({ + data: { notifications: notifs }, + error: null, + }); + + const { notificationStore, notifications } = await import('../notificationStore'); + await notificationStore.load(); + + expect(get(notifications)).toHaveLength(2); + expect(get(notifications)[0].title).toBe('First'); + }); + }); +}); diff --git a/frontend/src/stores/__tests__/theme.test.ts b/frontend/src/stores/__tests__/theme.test.ts new file mode 100644 index 00000000..b2b75ba6 --- /dev/null +++ b/frontend/src/stores/__tests__/theme.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { get } from 'svelte/store'; + +// Mock the dynamic imports before importing the theme module +vi.mock('../../lib/user-settings', () => ({ + saveThemeSetting: vi.fn().mockResolvedValue(true), +})); + +vi.mock('../auth', () => ({ + isAuthenticated: { + subscribe: vi.fn((fn) => { + fn(false); + return () => {}; + }), + }, +})); + +describe('theme store', () => { + let localStorageData: Record = {}; + + beforeEach(async () => { + // Reset localStorage mock + localStorageData = {}; + vi.mocked(localStorage.getItem).mockImplementation((key: string) => localStorageData[key] ?? null); + vi.mocked(localStorage.setItem).mockImplementation((key: string, value: string) => { + localStorageData[key] = value; + }); + + // Reset document mock + document.documentElement.classList.remove('dark'); + + // Clear module cache to reset store state + vi.resetModules(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('initial state', () => { + it('defaults to auto when no stored value', async () => { + const { theme } = await import('../theme'); + expect(get(theme)).toBe('auto'); + }); + + it('loads stored theme from localStorage', async () => { + localStorageData['app-theme'] = 'dark'; + const { theme } = await import('../theme'); + expect(get(theme)).toBe('dark'); + }); + + it('ignores invalid stored values', async () => { + localStorageData['app-theme'] = 'invalid'; + const { theme } = await import('../theme'); + expect(get(theme)).toBe('auto'); + }); + + it('accepts light theme from storage', async () => { + localStorageData['app-theme'] = 'light'; + const { theme } = await import('../theme'); + expect(get(theme)).toBe('light'); + }); + }); + + describe('theme.set', () => { + it('updates the store value', async () => { + const { theme } = await import('../theme'); + theme.set('dark'); + expect(get(theme)).toBe('dark'); + }); + + it('persists to localStorage', async () => { + const { theme } = await import('../theme'); + theme.set('dark'); + expect(localStorage.setItem).toHaveBeenCalledWith('app-theme', 'dark'); + }); + + it('applies dark class when set to dark', async () => { + const { theme } = await import('../theme'); + theme.set('dark'); + expect(document.documentElement.classList.contains('dark')).toBe(true); + }); + + it('removes dark class when set to light', async () => { + document.documentElement.classList.add('dark'); + const { theme } = await import('../theme'); + theme.set('light'); + expect(document.documentElement.classList.contains('dark')).toBe(false); + }); + }); + + describe('toggleTheme', () => { + it('cycles from light to dark', async () => { + localStorageData['app-theme'] = 'light'; + const { theme, toggleTheme } = await import('../theme'); + + toggleTheme(); + expect(get(theme)).toBe('dark'); + }); + + it('cycles from dark to auto', async () => { + localStorageData['app-theme'] = 'dark'; + const { theme, toggleTheme } = await import('../theme'); + + toggleTheme(); + expect(get(theme)).toBe('auto'); + }); + + it('cycles from auto to light', async () => { + const { theme, toggleTheme } = await import('../theme'); + // Default is auto + toggleTheme(); + expect(get(theme)).toBe('light'); + }); + + it('completes full cycle', async () => { + const { theme, toggleTheme } = await import('../theme'); + + expect(get(theme)).toBe('auto'); + toggleTheme(); + expect(get(theme)).toBe('light'); + toggleTheme(); + expect(get(theme)).toBe('dark'); + toggleTheme(); + expect(get(theme)).toBe('auto'); + }); + }); + + describe('setTheme', () => { + it('sets theme to specified value', async () => { + const { theme, setTheme } = await import('../theme'); + + setTheme('dark'); + expect(get(theme)).toBe('dark'); + + setTheme('light'); + expect(get(theme)).toBe('light'); + + setTheme('auto'); + expect(get(theme)).toBe('auto'); + }); + + it('persists to localStorage', async () => { + const { setTheme } = await import('../theme'); + setTheme('dark'); + expect(localStorage.setItem).toHaveBeenCalledWith('app-theme', 'dark'); + }); + }); + + describe('setThemeLocal', () => { + it('updates store without triggering server save', async () => { + const { theme, setThemeLocal } = await import('../theme'); + + setThemeLocal('dark'); + expect(get(theme)).toBe('dark'); + expect(localStorage.setItem).toHaveBeenCalledWith('app-theme', 'dark'); + }); + + it('applies theme to DOM', async () => { + const { setThemeLocal } = await import('../theme'); + + setThemeLocal('dark'); + expect(document.documentElement.classList.contains('dark')).toBe(true); + + setThemeLocal('light'); + expect(document.documentElement.classList.contains('dark')).toBe(false); + }); + }); + + describe('auto theme', () => { + it('applies light when system prefers light', async () => { + vi.mocked(matchMedia).mockImplementation((query: string) => ({ + matches: false, // Not dark + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + + const { theme } = await import('../theme'); + theme.set('auto'); + + expect(document.documentElement.classList.contains('dark')).toBe(false); + }); + + it('applies dark when system prefers dark', async () => { + vi.mocked(matchMedia).mockImplementation((query: string) => ({ + matches: query === '(prefers-color-scheme: dark)', // Is dark + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + + vi.resetModules(); + const { theme } = await import('../theme'); + theme.set('auto'); + + expect(document.documentElement.classList.contains('dark')).toBe(true); + }); + }); + + describe('subscription', () => { + it('notifies subscribers on change', async () => { + const { theme } = await import('../theme'); + const values: string[] = []; + + const unsubscribe = theme.subscribe((value) => { + values.push(value); + }); + + theme.set('dark'); + theme.set('light'); + + expect(values).toContain('dark'); + expect(values).toContain('light'); + + unsubscribe(); + }); + }); +}); diff --git a/frontend/src/stores/__tests__/toastStore.test.ts b/frontend/src/stores/__tests__/toastStore.test.ts new file mode 100644 index 00000000..145eb2c7 --- /dev/null +++ b/frontend/src/stores/__tests__/toastStore.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { get } from 'svelte/store'; +import { toasts, addToast, removeToast, TOAST_DURATION } from '../toastStore'; + +describe('toastStore', () => { + beforeEach(() => { + vi.useFakeTimers(); + // Reset store state + toasts.set([]); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('addToast', () => { + it('adds a toast with generated id', () => { + addToast('Test message', 'success'); + const current = get(toasts); + + expect(current).toHaveLength(1); + expect(current[0].message).toBe('Test message'); + expect(current[0].type).toBe('success'); + expect(current[0].id).toBeDefined(); + }); + + it('defaults to info type when not specified', () => { + addToast('Info message'); + const current = get(toasts); + + expect(current[0].type).toBe('info'); + }); + + it('handles object messages with message property', () => { + addToast({ message: 'Object message' }, 'warning'); + const current = get(toasts); + + expect(current[0].message).toBe('Object message'); + }); + + it('handles object messages with detail property', () => { + addToast({ detail: 'Detail message' }, 'error'); + const current = get(toasts); + + expect(current[0].message).toBe('Detail message'); + }); + + it('prefers message over detail property', () => { + addToast({ message: 'Message', detail: 'Detail' }, 'info'); + const current = get(toasts); + + expect(current[0].message).toBe('Message'); + }); + + it('converts non-string values to string', () => { + addToast(42, 'info'); + const current = get(toasts); + + expect(current[0].message).toBe('42'); + }); + + it('handles null values', () => { + addToast(null, 'info'); + const current = get(toasts); + + expect(current[0].message).toBe('null'); + }); + + it('can add multiple toasts', () => { + addToast('First', 'info'); + addToast('Second', 'success'); + addToast('Third', 'error'); + const current = get(toasts); + + expect(current).toHaveLength(3); + expect(current[0].message).toBe('First'); + expect(current[1].message).toBe('Second'); + expect(current[2].message).toBe('Third'); + }); + + it('generates unique ids for each toast', () => { + addToast('First', 'info'); + addToast('Second', 'info'); + const current = get(toasts); + + expect(current[0].id).not.toBe(current[1].id); + }); + }); + + describe('removeToast', () => { + it('removes a toast by id', () => { + addToast('Test', 'info'); + const [toast] = get(toasts); + + removeToast(toast.id); + + expect(get(toasts)).toHaveLength(0); + }); + + it('removes only the specified toast', () => { + addToast('First', 'info'); + addToast('Second', 'success'); + const [first] = get(toasts); + + removeToast(first.id); + const remaining = get(toasts); + + expect(remaining).toHaveLength(1); + expect(remaining[0].message).toBe('Second'); + }); + + it('does nothing when id not found', () => { + addToast('Test', 'info'); + + removeToast('nonexistent-id'); + + expect(get(toasts)).toHaveLength(1); + }); + }); + + describe('auto-removal', () => { + it('auto-removes toast after TOAST_DURATION', () => { + addToast('Temporary', 'warning'); + + expect(get(toasts)).toHaveLength(1); + + vi.advanceTimersByTime(TOAST_DURATION + 100); + + expect(get(toasts)).toHaveLength(0); + }); + + it('does not remove toast before TOAST_DURATION', () => { + addToast('Temporary', 'warning'); + + vi.advanceTimersByTime(TOAST_DURATION - 100); + + expect(get(toasts)).toHaveLength(1); + }); + + it('removes multiple toasts at their respective times', () => { + addToast('First', 'info'); + + vi.advanceTimersByTime(1000); + addToast('Second', 'success'); + + // First toast should be removed after its duration (4000ms remaining) + vi.advanceTimersByTime(TOAST_DURATION - 1000); + expect(get(toasts)).toHaveLength(1); + expect(get(toasts)[0].message).toBe('Second'); + + // Second toast should be removed after its full duration (1000ms remaining) + vi.advanceTimersByTime(1000); + expect(get(toasts)).toHaveLength(0); + }); + }); + + describe('TOAST_DURATION constant', () => { + it('is exported and equals 5000ms', () => { + expect(TOAST_DURATION).toBe(5000); + }); + }); +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 00000000..ba51bb5e --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'vitest/config'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { svelteTesting } from '@testing-library/svelte/vite'; + +export default defineConfig({ + plugins: [ + svelte({ + hot: !process.env.VITEST, + compilerOptions: { + dev: true, + runes: true, + }, + }), + svelteTesting(), + ], + test: { + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + include: ['src/**/*.{test,spec}.{js,ts}'], + globals: true, + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + include: ['src/**/*.{ts,svelte}'], + exclude: ['src/lib/api/**', 'src/**/*.test.ts'], + }, + }, + resolve: { + conditions: ['browser'], + alias: { + $lib: '/src/lib', + }, + }, +}); diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts new file mode 100644 index 00000000..699fdf9b --- /dev/null +++ b/frontend/vitest.setup.ts @@ -0,0 +1,75 @@ +import '@testing-library/jest-dom/vitest'; +import { vi } from 'vitest'; + +// Mock localStorage +const localStorageStore: Record = {}; +const localStorageMock = { + getItem: vi.fn((key: string) => localStorageStore[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + localStorageStore[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete localStorageStore[key]; + }), + clear: vi.fn(() => { + Object.keys(localStorageStore).forEach(key => delete localStorageStore[key]); + }), + get length() { + return Object.keys(localStorageStore).length; + }, + key: vi.fn((index: number) => Object.keys(localStorageStore)[index] ?? null), +}; +vi.stubGlobal('localStorage', localStorageMock); + +// Mock sessionStorage +const sessionStorageStore: Record = {}; +const sessionStorageMock = { + getItem: vi.fn((key: string) => sessionStorageStore[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + sessionStorageStore[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete sessionStorageStore[key]; + }), + clear: vi.fn(() => { + Object.keys(sessionStorageStore).forEach(key => delete sessionStorageStore[key]); + }), + get length() { + return Object.keys(sessionStorageStore).length; + }, + key: vi.fn((index: number) => Object.keys(sessionStorageStore)[index] ?? null), +}; +vi.stubGlobal('sessionStorage', sessionStorageMock); + +// Mock matchMedia for theme tests +vi.stubGlobal('matchMedia', vi.fn().mockImplementation((query: string) => ({ + matches: query === '(prefers-color-scheme: dark)' ? false : false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), +}))); + +// Mock ResizeObserver +vi.stubGlobal('ResizeObserver', vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +}))); + +// Mock IntersectionObserver +vi.stubGlobal('IntersectionObserver', vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +}))); + +// Helper to reset mocks between tests +export function resetStorageMocks() { + Object.keys(localStorageStore).forEach(key => delete localStorageStore[key]); + Object.keys(sessionStorageStore).forEach(key => delete sessionStorageStore[key]); + vi.clearAllMocks(); +} From 0eb35ca405dcac3938075d97d7fc103e13cbfb8a Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 21 Dec 2025 23:07:52 +0100 Subject: [PATCH 02/32] deps fix + adding folders with frontend test results to gitignore --- .gitignore | 5 + frontend/package-lock.json | 615 +++++++++++++++++++++++++++++++++++++ frontend/package.json | 7 +- 3 files changed, 624 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4249f3c0..ea473114 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,11 @@ yarn-error.log* # Build outputs frontend/public/build/ +# Frontend testing +frontend/coverage/ +frontend/playwright-report/ +frontend/test-results/ + # Helm helm/*/charts/*.tgz helm/*/Chart.lock diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7150b51..dafcc53c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -52,6 +52,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/svelte": "^5.2.6", "@testing-library/user-event": "^14.6.1", + "@vitest/coverage-v8": "^3.2.4", "express": "^5.2.1", "http-proxy": "^1.18.1", "jsdom": "^26.1.0", @@ -80,6 +81,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -107,6 +121,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", @@ -116,6 +139,21 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -125,6 +163,28 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", @@ -822,6 +882,23 @@ "node": ">=20" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -834,6 +911,15 @@ "node": ">=18.0.0" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -933,6 +1019,16 @@ "svelte": "^5.0.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@playwright/test": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", @@ -1954,6 +2050,48 @@ "@codemirror/view": ">=6.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -2214,6 +2352,32 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.9.tgz", + "integrity": "sha512-dSC6tJeOJxbZrPzPbv5mMd6CMiQ1ugaVXXPRad2fXUSsy1kstFn9XQWemV9VW7Y7kpxgQ/4WMoZfwdH8XSU48w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2701,6 +2865,20 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -3104,6 +3282,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3115,6 +3299,12 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3395,6 +3585,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3603,6 +3809,12 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -3798,6 +4010,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3887,6 +4108,77 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -4314,6 +4606,32 @@ "node": ">=12" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4651,6 +4969,12 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -4684,11 +5008,36 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -5858,6 +6207,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -5936,6 +6306,18 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -6032,6 +6414,96 @@ "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -6304,6 +6776,55 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6835,6 +7356,21 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -6851,6 +7387,85 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index d3915732..d2ae32db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@codemirror/state": "^6.4.1", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.34.1", + "@mateothegreat/svelte5-router": "^2.16.19", "@rollup/plugin-commonjs": "^24.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.0.0", @@ -45,11 +46,11 @@ "rollup-plugin-svelte": "^7.2.2", "sirv-cli": "^3.0.1", "svelte": "^5.46.0", - "svelte-preprocess": "^6.0.3", - "@mateothegreat/svelte5-router": "^2.16.19" + "svelte-preprocess": "^6.0.3" }, "devDependencies": { "@babel/runtime": "^7.24.7", + "@hey-api/openapi-ts": "0.89.1", "@playwright/test": "^1.52.0", "@rollup/plugin-typescript": "^12.1.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", @@ -58,9 +59,9 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/svelte": "^5.2.6", "@testing-library/user-event": "^14.6.1", + "@vitest/coverage-v8": "^3.2.4", "express": "^5.2.1", "http-proxy": "^1.18.1", - "@hey-api/openapi-ts": "0.89.1", "jsdom": "^26.1.0", "rollup-plugin-serve": "^1.1.1", "tailwindcss": "^4.1.13", From 519b53b41353b6cf7dde4ad50cba167f8eabc72c Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 21 Dec 2025 23:11:05 +0100 Subject: [PATCH 03/32] sonarqube issue fix --- frontend/vitest.setup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts index 699fdf9b..fc0367b6 100644 --- a/frontend/vitest.setup.ts +++ b/frontend/vitest.setup.ts @@ -41,9 +41,9 @@ const sessionStorageMock = { }; vi.stubGlobal('sessionStorage', sessionStorageMock); -// Mock matchMedia for theme tests +// Mock matchMedia for theme tests (defaults to light mode) vi.stubGlobal('matchMedia', vi.fn().mockImplementation((query: string) => ({ - matches: query === '(prefers-color-scheme: dark)' ? false : false, + matches: false, media: query, onchange: null, addListener: vi.fn(), From 07ed7842ea2865fac3c81b8f5a553aea6ed4ffcb Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 21 Dec 2025 23:19:46 +0100 Subject: [PATCH 04/32] ci frontend tests fix --- .github/workflows/frontend-tests.yml | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index d41859fc..c9087de3 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -53,6 +53,21 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + + - name: Install Playwright browsers + working-directory: frontend + run: npx playwright install chromium + - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 @@ -152,21 +167,6 @@ jobs: run: | curl --retry 30 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:5001/ || true - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install frontend dependencies - working-directory: frontend - run: npm ci - - - name: Install Playwright browsers - working-directory: frontend - run: npx playwright install chromium - - name: Run E2E tests working-directory: frontend env: From cc85475a5f2b3244a0422c66a10fa47707ece968 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 21 Dec 2025 23:27:41 +0100 Subject: [PATCH 05/32] tests fix --- frontend/e2e/theme.spec.ts | 58 ++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/frontend/e2e/theme.spec.ts b/frontend/e2e/theme.spec.ts index f71a50a9..7c413323 100644 --- a/frontend/e2e/theme.spec.ts +++ b/frontend/e2e/theme.spec.ts @@ -1,42 +1,35 @@ import { test, expect } from '@playwright/test'; test.describe('Theme', () => { - test.beforeEach(async ({ page }) => { - // Clear localStorage to start fresh + test('auto theme follows system light preference', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); await page.goto('/login'); await page.evaluate(() => localStorage.removeItem('app-theme')); - }); - - test('defaults to auto theme (no dark class)', async ({ page }) => { - await page.goto('/login'); + await page.reload(); - // Check that dark class is not present initially (assuming system prefers light) const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') ); - // This may be true or false depending on system preference - expect(typeof hasDarkClass).toBe('boolean'); + expect(hasDarkClass).toBe(false); }); - test('persists theme preference in localStorage', async ({ page }) => { + test('auto theme follows system dark preference', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'dark' }); await page.goto('/login'); - - // Set theme via localStorage - await page.evaluate(() => localStorage.setItem('app-theme', 'dark')); + await page.evaluate(() => localStorage.removeItem('app-theme')); await page.reload(); - const storedTheme = await page.evaluate(() => localStorage.getItem('app-theme')); - expect(storedTheme).toBe('dark'); + const hasDarkClass = await page.evaluate(() => + document.documentElement.classList.contains('dark') + ); + expect(hasDarkClass).toBe(true); }); - test('applies dark theme when set', async ({ page }) => { + test('explicit dark theme overrides system preference', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); await page.goto('/login'); - - // Set dark theme - await page.evaluate(() => { - localStorage.setItem('app-theme', 'dark'); - document.documentElement.classList.add('dark'); - }); + await page.evaluate(() => localStorage.setItem('app-theme', 'dark')); + await page.reload(); const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') @@ -44,14 +37,11 @@ test.describe('Theme', () => { expect(hasDarkClass).toBe(true); }); - test('applies light theme when set', async ({ page }) => { + test('explicit light theme overrides system preference', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'dark' }); await page.goto('/login'); - - // Set light theme - await page.evaluate(() => { - localStorage.setItem('app-theme', 'light'); - document.documentElement.classList.remove('dark'); - }); + await page.evaluate(() => localStorage.setItem('app-theme', 'light')); + await page.reload(); const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') @@ -61,15 +51,15 @@ test.describe('Theme', () => { test('theme persists across page navigation', async ({ page }) => { await page.goto('/login'); - - // Set dark theme await page.evaluate(() => localStorage.setItem('app-theme', 'dark')); - await page.reload(); - - // Navigate to another page await page.goto('/register'); const storedTheme = await page.evaluate(() => localStorage.getItem('app-theme')); expect(storedTheme).toBe('dark'); + + const hasDarkClass = await page.evaluate(() => + document.documentElement.classList.contains('dark') + ); + expect(hasDarkClass).toBe(true); }); }); From 578efc311f017bf5797c7ef45fe8ca0a3f92211a Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 21 Dec 2025 23:30:03 +0100 Subject: [PATCH 06/32] ci timeouts added (gha - 60s/10s, local - 30s/5s) --- frontend/playwright.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 719b310b..3eb14204 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -6,6 +6,10 @@ export default defineConfig({ forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, + timeout: process.env.CI ? 60000 : 30000, + expect: { + timeout: process.env.CI ? 10000 : 5000, + }, reporter: 'html', use: { baseURL: 'https://localhost:5001', From 635003316d98124c55be048c95a07272b3299cce Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 00:03:28 +0100 Subject: [PATCH 07/32] ci fix: - frontend without docker call, directly npm test - backend with setup ci compose --- .github/actions/setup-ci-compose/action.yml | 55 +++++ .github/workflows/ci.yml | 226 ++++++++++++++++++++ .github/workflows/frontend-tests.yml | 196 ----------------- .github/workflows/tests.yml | 222 ------------------- README.md | 9 +- deploy.sh | 2 +- frontend/playwright.config.ts | 5 +- 7 files changed, 288 insertions(+), 427 deletions(-) create mode 100644 .github/actions/setup-ci-compose/action.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/frontend-tests.yml delete mode 100644 .github/workflows/tests.yml diff --git a/.github/actions/setup-ci-compose/action.yml b/.github/actions/setup-ci-compose/action.yml new file mode 100644 index 00000000..6a51af4d --- /dev/null +++ b/.github/actions/setup-ci-compose/action.yml @@ -0,0 +1,55 @@ +name: Setup CI Compose +description: Creates docker-compose.ci.yaml with CI-specific modifications + +inputs: + kubeconfig-path: + description: Path to kubeconfig file for cert-generator mount + required: true + +runs: + using: composite + steps: + - name: Install yq + shell: bash + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Create CI compose configuration + shell: bash + env: + KUBECONFIG_PATH: ${{ inputs.kubeconfig-path }} + run: | + cp docker-compose.yaml docker-compose.ci.yaml + + # Backend environment variables + yq eval '.services.backend.environment += ["TESTING=true"]' -i docker-compose.ci.yaml + yq eval '.services.backend.environment += ["MONGO_ROOT_USER=root"]' -i docker-compose.ci.yaml + yq eval '.services.backend.environment += ["MONGO_ROOT_PASSWORD=rootpassword"]' -i docker-compose.ci.yaml + yq eval '.services.backend.environment += ["OTEL_SDK_DISABLED=true"]' -i docker-compose.ci.yaml + + # Remove hot-reload volume mounts (causes permission issues in CI) + yq eval '.services.backend.volumes = [.services.backend.volumes[] | select(. != "./backend:/app")]' -i docker-compose.ci.yaml + yq eval '.services."k8s-worker".volumes = [.services."k8s-worker".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml + yq eval '.services."pod-monitor".volumes = [.services."pod-monitor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml + yq eval '.services."result-processor".volumes = [.services."result-processor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml + + # Disable Kafka SASL authentication for CI + yq eval 'del(.services.kafka.environment.KAFKA_OPTS)' -i docker-compose.ci.yaml + yq eval 'del(.services.zookeeper.environment.KAFKA_OPTS)' -i docker-compose.ci.yaml + yq eval 'del(.services.zookeeper.environment.ZOOKEEPER_AUTH_PROVIDER_1)' -i docker-compose.ci.yaml + yq eval '.services.kafka.volumes = [.services.kafka.volumes[] | select(. | contains("jaas.conf") | not)]' -i docker-compose.ci.yaml + yq eval '.services.zookeeper.volumes = [.services.zookeeper.volumes[] | select(. | contains("/etc/kafka") | not)]' -i docker-compose.ci.yaml + + # Simplify Zookeeper for CI + yq eval '.services.zookeeper.environment.ZOOKEEPER_4LW_COMMANDS_WHITELIST = "ruok,srvr"' -i docker-compose.ci.yaml + yq eval 'del(.services.zookeeper.healthcheck)' -i docker-compose.ci.yaml + yq eval '.services.kafka.depends_on.zookeeper.condition = "service_started"' -i docker-compose.ci.yaml + + # Cert-generator CI configuration + yq eval 'select(.services."cert-generator".extra_hosts == null).services."cert-generator".extra_hosts = []' -i docker-compose.ci.yaml + yq eval '.services."cert-generator".extra_hosts += ["host.docker.internal:host-gateway"]' -i docker-compose.ci.yaml + yq eval '.services."cert-generator".environment += ["CI=true"]' -i docker-compose.ci.yaml + yq eval ".services.\"cert-generator\".volumes += [\"${KUBECONFIG_PATH}:/root/.kube/config:ro\"]" -i docker-compose.ci.yaml + + echo "Created docker-compose.ci.yaml" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..65c28bcb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,226 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + workflow_dispatch: + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + # ============================================================ + # Frontend Unit Tests (no docker needed) + # ============================================================ + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + + - name: Run frontend unit tests + working-directory: frontend + run: npm test + + - name: Run frontend tests with coverage + working-directory: frontend + run: npm run test:coverage + + - name: Upload frontend coverage + uses: actions/upload-artifact@v6 + if: always() + with: + name: frontend-coverage + path: frontend/coverage/ + + # ============================================================ + # Infrastructure Setup (for backend + E2E tests) + # ============================================================ + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Setup Kubernetes (k3s) + run: | + curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --tls-san host.docker.internal" sh - + mkdir -p $HOME/.kube + sudo k3s kubectl config view --raw > $HOME/.kube/config + sudo chmod 600 $HOME/.kube/config + timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done' + kubectl version + kubectl get nodes + + - name: Create kubeconfig for CI + run: | + cat > backend/kubeconfig.yaml < logs/docker-compose.log + docker compose -f docker-compose.ci.yaml logs cert-generator > logs/cert-generator.log + docker compose -f docker-compose.ci.yaml logs backend > logs/backend.log + docker compose -f docker-compose.ci.yaml logs frontend > logs/frontend.log + docker compose -f docker-compose.ci.yaml logs mongo > logs/mongo.log + kubectl get events --sort-by='.metadata.creationTimestamp' > logs/k8s-events.log 2>&1 || true + kubectl get pods -A -o wide > logs/k8s-pods-final.log 2>&1 || true + kubectl describe pods -A > logs/k8s-describe-pods-final.log 2>&1 || true + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v6 + with: + name: ci-logs + path: logs/ diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml deleted file mode 100644 index c9087de3..00000000 --- a/.github/workflows/frontend-tests.yml +++ /dev/null @@ -1,196 +0,0 @@ -name: Frontend Tests - -on: - push: - branches: [main, dev] - paths: - - 'frontend/**' - - '.github/workflows/frontend-tests.yml' - pull_request: - branches: [main, dev] - paths: - - 'frontend/**' - - '.github/workflows/frontend-tests.yml' - workflow_dispatch: - -jobs: - unit-tests: - name: Unit Tests - runs-on: ubuntu-latest - defaults: - run: - working-directory: frontend - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install dependencies - run: npm ci - - - name: Run unit tests - run: npm test - - - name: Run tests with coverage - run: npm run test:coverage - - - name: Upload coverage report - uses: actions/upload-artifact@v4 - if: always() - with: - name: frontend-coverage - path: frontend/coverage/ - - e2e-tests: - name: E2E Tests - runs-on: ubuntu-latest - needs: unit-tests - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - - name: Install frontend dependencies - working-directory: frontend - run: npm ci - - - name: Install Playwright browsers - working-directory: frontend - run: npx playwright install chromium - - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Install yq - run: | - sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq - sudo chmod +x /usr/local/bin/yq - - - name: Setup Kubernetes (k3s) - run: | - curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --tls-san host.docker.internal" sh - - mkdir -p $HOME/.kube - sudo k3s kubectl config view --raw > $HOME/.kube/config - sudo chmod 600 $HOME/.kube/config - timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done' - - - name: Create kubeconfig for CI - run: | - cat > backend/kubeconfig.yaml < logs/docker-compose.log - docker compose -f docker-compose.ci.yaml logs frontend > logs/frontend.log - docker compose -f docker-compose.ci.yaml logs backend > logs/backend.log - - - name: Upload logs - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-test-logs - path: logs/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 153e51c6..00000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,222 +0,0 @@ -name: Integration Tests - -on: - push: - branches: [ main, dev ] - pull_request: - branches: [ main, dev ] - workflow_dispatch: - -jobs: - tests: - name: Backend Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Install yq - run: | - sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq - sudo chmod +x /usr/local/bin/yq - - - name: Setup Kubernetes (k3s) and Kubeconfig - run: | - curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --tls-san host.docker.internal" sh - - mkdir -p $HOME/.kube - sudo k3s kubectl config view --raw > $HOME/.kube/config - sudo chmod 600 $HOME/.kube/config - timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done' - kubectl version - kubectl get nodes - - - name: Create dummy kubeconfig for CI - run: | - # Create a dummy kubeconfig so backend can start without real k8s connection - cat > backend/kubeconfig.yaml < logs/docker-compose.log - docker compose -f docker-compose.ci.yaml logs cert-generator > logs/cert-generator.log - docker compose -f docker-compose.ci.yaml logs backend > logs/backend.log - docker compose -f docker-compose.ci.yaml logs mongo > logs/mongo.log - kubectl get events --sort-by='.metadata.creationTimestamp' > logs/k8s-events.log - kubectl get pods -A -o wide > logs/k8s-pods-final.log - kubectl describe pods -A > logs/k8s-describe-pods-final.log - - - name: Upload logs - if: always() - uses: actions/upload-artifact@v6 - with: - name: integration-test-logs - path: logs/ diff --git a/README.md b/README.md index 30ddba6e..95042410 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,11 @@ Docker Scan Status - - Backend Tests Status - - - Frontend Tests Status + + Tests Status - Backend Coverage + Coverage

diff --git a/deploy.sh b/deploy.sh index cdf500dd..6819b109 100755 --- a/deploy.sh +++ b/deploy.sh @@ -101,7 +101,7 @@ cmd_dev() { echo "" echo "Services:" echo " Backend: https://localhost:443" - echo " Frontend: http://localhost:5001" + echo " Frontend: https://localhost:5001" echo " Kafdrop: http://localhost:9000" echo " Jaeger: http://localhost:16686" echo " Grafana: http://localhost:3000" diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 3eb14204..a7d64970 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -23,10 +23,11 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, ], - webServer: { + // In CI, frontend runs via docker-compose; locally, start dev server if needed + webServer: process.env.CI ? undefined : { command: 'npm run dev', url: 'https://localhost:5001', - reuseExistingServer: !process.env.CI, + reuseExistingServer: true, ignoreHTTPSErrors: true, timeout: 120000, }, From 605506904e1a3c032af25f728902ffb08140e7f1 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 00:08:41 +0100 Subject: [PATCH 08/32] env home fix in ci --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65c28bcb..bc4ea96c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,9 +52,10 @@ jobs: - name: Setup Kubernetes (k3s) run: | curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --tls-san host.docker.internal" sh - - mkdir -p $HOME/.kube - sudo k3s kubectl config view --raw > $HOME/.kube/config - sudo chmod 600 $HOME/.kube/config + mkdir -p /home/runner/.kube + sudo k3s kubectl config view --raw > /home/runner/.kube/config + sudo chmod 600 /home/runner/.kube/config + export KUBECONFIG=/home/runner/.kube/config timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done' kubectl version kubectl get nodes @@ -84,7 +85,7 @@ jobs: - name: Setup CI Compose uses: ./.github/actions/setup-ci-compose with: - kubeconfig-path: ${{ env.HOME }}/.kube/config + kubeconfig-path: /home/runner/.kube/config - name: Pre-pull base images run: | From a0a76e747c272a95940faab825f4f10cb02c09b5 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 00:34:37 +0100 Subject: [PATCH 09/32] ci frontend e2e tests fixes --- .github/actions/setup-ci-compose/action.yml | 7 +++---- .github/workflows/ci.yml | 2 +- frontend/e2e/auth.spec.ts | 14 +++++++------- frontend/playwright.config.ts | 10 +++++----- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/actions/setup-ci-compose/action.yml b/.github/actions/setup-ci-compose/action.yml index 6a51af4d..97aec7eb 100644 --- a/.github/actions/setup-ci-compose/action.yml +++ b/.github/actions/setup-ci-compose/action.yml @@ -12,7 +12,7 @@ runs: - name: Install yq shell: bash run: | - sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.50.1/yq_linux_amd64 sudo chmod +x /usr/local/bin/yq - name: Create CI compose configuration @@ -47,9 +47,8 @@ runs: yq eval '.services.kafka.depends_on.zookeeper.condition = "service_started"' -i docker-compose.ci.yaml # Cert-generator CI configuration - yq eval 'select(.services."cert-generator".extra_hosts == null).services."cert-generator".extra_hosts = []' -i docker-compose.ci.yaml - yq eval '.services."cert-generator".extra_hosts += ["host.docker.internal:host-gateway"]' -i docker-compose.ci.yaml - yq eval '.services."cert-generator".environment += ["CI=true"]' -i docker-compose.ci.yaml + yq eval '.services."cert-generator".extra_hosts = ((.services."cert-generator".extra_hosts // []) + ["host.docker.internal:host-gateway"] | unique)' -i docker-compose.ci.yaml + yq eval '.services."cert-generator".environment = ((.services."cert-generator".environment // []) + ["CI=true"] | unique)' -i docker-compose.ci.yaml yq eval ".services.\"cert-generator\".volumes += [\"${KUBECONFIG_PATH}:/root/.kube/config:ro\"]" -i docker-compose.ci.yaml echo "Created docker-compose.ci.yaml" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc4ea96c..ad139eca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -174,7 +174,7 @@ jobs: if: always() with: token: ${{ secrets.CODECOV_TOKEN }} - file: backend/coverage.xml + files: backend/coverage.xml flags: backend name: backend-coverage slug: HardMax71/Integr8sCode diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index a38b96c3..87e6f607 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -31,7 +31,7 @@ test.describe('Authentication', () => { await page.click('button[type="submit"]'); // Wait for error message to appear - await expect(page.locator('p.text-red-600, p.text-red-400')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('p.text-red-600, p.text-red-400')).toBeVisible(); }); test('redirects to editor on successful login', async ({ page }) => { @@ -43,7 +43,7 @@ test.describe('Authentication', () => { await page.click('button[type="submit"]'); // Should redirect to editor - await expect(page).toHaveURL(/\/editor/, { timeout: 15000 }); + await expect(page).toHaveURL(/\/editor/); }); test('shows loading state during login', async ({ page }) => { @@ -65,7 +65,7 @@ test.describe('Authentication', () => { await page.goto('/editor'); // Should redirect to login - await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); + await expect(page).toHaveURL(/\/login/); }); test('preserves redirect path after login', async ({ page }) => { @@ -73,7 +73,7 @@ test.describe('Authentication', () => { await page.goto('/settings'); // Should redirect to login - await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); + await expect(page).toHaveURL(/\/login/); // Login await page.fill('#username', 'user'); @@ -81,7 +81,7 @@ test.describe('Authentication', () => { await page.click('button[type="submit"]'); // Should redirect back to settings - await expect(page).toHaveURL(/\/settings/, { timeout: 15000 }); + await expect(page).toHaveURL(/\/settings/); }); test('has link to registration page', async ({ page }) => { @@ -108,7 +108,7 @@ test.describe('Logout', () => { await page.fill('#username', 'user'); await page.fill('#password', 'user123'); await page.click('button[type="submit"]'); - await expect(page).toHaveURL(/\/editor/, { timeout: 15000 }); + await expect(page).toHaveURL(/\/editor/); }); test('can logout from authenticated state', async ({ page }) => { @@ -118,7 +118,7 @@ test.describe('Logout', () => { if (await logoutButton.isVisible()) { await logoutButton.click(); // Should redirect to login - await expect(page).toHaveURL(/\/login/, { timeout: 10000 }); + await expect(page).toHaveURL(/\/login/); } }); }); diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index a7d64970..f88b8f66 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -4,13 +4,13 @@ export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - timeout: process.env.CI ? 60000 : 30000, + retries: process.env.CI ? 1 : 0, // Reduced: 1 retry is enough to catch flakes + workers: process.env.CI ? 2 : undefined, // Increased: tests are independent + timeout: 30000, // 30s is plenty for page operations expect: { - timeout: process.env.CI ? 10000 : 5000, + timeout: 5000, // 5s for element expectations }, - reporter: 'html', + reporter: process.env.CI ? [['html'], ['github']] : 'html', use: { baseURL: 'https://localhost:5001', ignoreHTTPSErrors: true, From 7d1abca436b76e3d404a8a1a843049e3f31e0825 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 00:54:17 +0100 Subject: [PATCH 10/32] ci tests running in parallel (frontend+backend), fixing of e2e tests --- .github/workflows/ci.yml | 259 +++++++++++++++++++++++++------------ frontend/e2e/auth.spec.ts | 45 +++++-- frontend/e2e/theme.spec.ts | 17 ++- 3 files changed, 224 insertions(+), 97 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad139eca..ff87a361 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,15 +8,15 @@ on: workflow_dispatch: jobs: - test: - name: Tests + # ============================================================ + # Phase 1: Unit tests run immediately in parallel (no docker) + # ============================================================ + frontend-unit: + name: Frontend Unit Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - # ============================================================ - # Frontend Unit Tests (no docker needed) - # ============================================================ - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -24,28 +24,82 @@ jobs: cache: 'npm' cache-dependency-path: frontend/package-lock.json - - name: Install frontend dependencies + - name: Install dependencies working-directory: frontend run: npm ci - - name: Run frontend unit tests + - name: Run unit tests working-directory: frontend run: npm test - - name: Run frontend tests with coverage + - name: Run tests with coverage working-directory: frontend run: npm run test:coverage - - name: Upload frontend coverage + - name: Upload coverage uses: actions/upload-artifact@v6 if: always() with: name: frontend-coverage path: frontend/coverage/ - # ============================================================ - # Infrastructure Setup (for backend + E2E tests) - # ============================================================ + backend-unit: + name: Backend Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: "backend/uv.lock" + + - name: Install Python dependencies + run: | + cd backend + uv python install 3.12 + uv sync --frozen + + - name: Run unit tests + timeout-minutes: 5 + run: | + cd backend + uv run pytest tests/unit -v --cov=app --cov-branch --cov-report=xml --cov-report=term + + - name: Upload coverage + uses: actions/upload-artifact@v6 + if: always() + with: + name: backend-unit-coverage + path: backend/coverage.xml + + # ============================================================ + # Phase 2: Integration/E2E tests run in parallel after unit tests + # Each job sets up its own infrastructure (docker cache is shared) + # ============================================================ + frontend-e2e: + name: Frontend E2E Tests + needs: frontend-unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Install Playwright browsers + working-directory: frontend + run: npx playwright install chromium + - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 @@ -57,8 +111,6 @@ jobs: sudo chmod 600 /home/runner/.kube/config export KUBECONFIG=/home/runner/.kube/config timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done' - kubectl version - kubectl get nodes - name: Create kubeconfig for CI run: | @@ -87,25 +139,6 @@ jobs: with: kubeconfig-path: /home/runner/.kube/config - - name: Pre-pull base images - run: | - docker pull python:3.12-slim & - docker pull ghcr.io/astral-sh/uv:0.9.18 & - docker pull node:22-alpine & - docker pull alpine:latest & - docker pull confluentinc/cp-kafka:7.5.0 & - docker pull confluentinc/cp-zookeeper:7.5.0 & - docker pull confluentinc/cp-schema-registry:7.5.0 & - docker pull mongo:8.0 & - docker pull redis:7-alpine & - docker pull grafana/grafana:latest & - docker pull jaegertracing/all-in-one:1.52 & - docker pull victoriametrics/victoria-metrics:v1.96.0 & - docker pull otel/opentelemetry-collector-contrib:0.91.0 & - docker pull obsidiandynamics/kafdrop:3.31.0 & - docker pull danielqsj/kafka-exporter:latest & - wait - - name: Build services uses: docker/bake-action@v6 with: @@ -125,23 +158,49 @@ jobs: docker compose -f docker-compose.ci.yaml up -d --remove-orphans docker compose -f docker-compose.ci.yaml ps - - name: Wait for backend + - name: Wait for services run: | + echo "Waiting for backend..." curl --retry 60 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:443/api/v1/health/live - docker compose -f docker-compose.ci.yaml ps + echo "Waiting for frontend..." + curl --retry 60 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:5001/ + echo "Services ready!" - - name: Wait for frontend - run: | - curl --retry 30 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:5001/ || true + - name: Run E2E tests + working-directory: frontend + env: + CI: true + run: npx playwright test --reporter=html + + - name: Upload Playwright report + uses: actions/upload-artifact@v6 + if: always() + with: + name: playwright-report + path: frontend/playwright-report/ - - name: Check K8s status + - name: Collect logs + if: failure() run: | - kubectl get pods -A -o wide - kubectl get services -A -o wide + mkdir -p logs + docker compose -f docker-compose.ci.yaml logs > logs/docker-compose.log + docker compose -f docker-compose.ci.yaml logs backend > logs/backend.log + docker compose -f docker-compose.ci.yaml logs frontend > logs/frontend.log + + - name: Upload logs + if: failure() + uses: actions/upload-artifact@v6 + with: + name: frontend-e2e-logs + path: logs/ + + backend-integration: + name: Backend Integration Tests + needs: backend-unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 - # ============================================================ - # Backend Tests - # ============================================================ - name: Set up uv uses: astral-sh/setup-uv@v7 with: @@ -154,8 +213,71 @@ jobs: uv python install 3.12 uv sync --frozen - - name: Run backend tests - id: backend-tests + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Setup Kubernetes (k3s) + run: | + curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --tls-san host.docker.internal" sh - + mkdir -p /home/runner/.kube + sudo k3s kubectl config view --raw > /home/runner/.kube/config + sudo chmod 600 /home/runner/.kube/config + export KUBECONFIG=/home/runner/.kube/config + timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done' + + - name: Create kubeconfig for CI + run: | + cat > backend/kubeconfig.yaml < logs/docker-compose.log - docker compose -f docker-compose.ci.yaml logs cert-generator > logs/cert-generator.log docker compose -f docker-compose.ci.yaml logs backend > logs/backend.log - docker compose -f docker-compose.ci.yaml logs frontend > logs/frontend.log docker compose -f docker-compose.ci.yaml logs mongo > logs/mongo.log kubectl get events --sort-by='.metadata.creationTimestamp' > logs/k8s-events.log 2>&1 || true - kubectl get pods -A -o wide > logs/k8s-pods-final.log 2>&1 || true - kubectl describe pods -A > logs/k8s-describe-pods-final.log 2>&1 || true + kubectl describe pods -A > logs/k8s-describe-pods.log 2>&1 || true - name: Upload logs - if: always() + if: failure() uses: actions/upload-artifact@v6 with: - name: ci-logs + name: backend-integration-logs path: logs/ diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index 87e6f607..73e89db9 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -2,12 +2,18 @@ import { test, expect } from '@playwright/test'; test.describe('Authentication', () => { test.beforeEach(async ({ page }) => { - // Clear any existing auth state - await page.context().clearCookies(); + // Clear all storage (localStorage stores auth state, not cookies) + await page.goto('/login'); + await page.evaluate(() => { + localStorage.clear(); + sessionStorage.clear(); + }); + await page.reload(); }); test('shows login page with form elements', async ({ page }) => { await page.goto('/login'); + await page.waitForLoadState('networkidle'); await expect(page.locator('h2')).toContainText('Sign in to your account'); await expect(page.locator('#username')).toBeVisible(); @@ -17,14 +23,15 @@ test.describe('Authentication', () => { test('shows validation when submitting empty form', async ({ page }) => { await page.goto('/login'); + await page.waitForLoadState('networkidle'); - // HTML5 validation should prevent submission const usernameInput = page.locator('#username'); await expect(usernameInput).toHaveAttribute('required', ''); }); test('shows error with invalid credentials', async ({ page }) => { await page.goto('/login'); + await page.waitForLoadState('networkidle'); await page.fill('#username', 'invaliduser'); await page.fill('#password', 'wrongpassword'); @@ -36,41 +43,50 @@ test.describe('Authentication', () => { test('redirects to editor on successful login', async ({ page }) => { await page.goto('/login'); + await page.waitForLoadState('networkidle'); - // Use test credentials (adjust based on your test environment) await page.fill('#username', 'user'); await page.fill('#password', 'user123'); await page.click('button[type="submit"]'); - // Should redirect to editor await expect(page).toHaveURL(/\/editor/); }); test('shows loading state during login', async ({ page }) => { await page.goto('/login'); + await page.waitForLoadState('networkidle'); await page.fill('#username', 'user'); await page.fill('#password', 'user123'); - // Start login but don't wait for it const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); - // Button should show loading text + // Button should show loading text or sign in await expect(submitButton).toContainText(/Logging in|Sign in/); }); test('redirects unauthenticated users from protected routes', async ({ page }) => { + // Ensure clean state + await page.goto('/login'); + await page.evaluate(() => localStorage.clear()); + // Try to access protected route await page.goto('/editor'); + await page.waitForLoadState('networkidle'); // Should redirect to login await expect(page).toHaveURL(/\/login/); }); test('preserves redirect path after login', async ({ page }) => { + // Ensure clean state + await page.goto('/login'); + await page.evaluate(() => localStorage.clear()); + // Try to access specific protected route await page.goto('/settings'); + await page.waitForLoadState('networkidle'); // Should redirect to login await expect(page).toHaveURL(/\/login/); @@ -86,6 +102,7 @@ test.describe('Authentication', () => { test('has link to registration page', async ({ page }) => { await page.goto('/login'); + await page.waitForLoadState('networkidle'); const registerLink = page.locator('a[href="/register"]'); await expect(registerLink).toBeVisible(); @@ -94,6 +111,7 @@ test.describe('Authentication', () => { test('can navigate to registration page', async ({ page }) => { await page.goto('/login'); + await page.waitForLoadState('networkidle'); await page.click('a[href="/register"]'); @@ -103,8 +121,16 @@ test.describe('Authentication', () => { test.describe('Logout', () => { test.beforeEach(async ({ page }) => { - // Login first + // Clear state first await page.goto('/login'); + await page.evaluate(() => { + localStorage.clear(); + sessionStorage.clear(); + }); + await page.reload(); + await page.waitForLoadState('networkidle'); + + // Login await page.fill('#username', 'user'); await page.fill('#password', 'user123'); await page.click('button[type="submit"]'); @@ -112,12 +138,11 @@ test.describe('Logout', () => { }); test('can logout from authenticated state', async ({ page }) => { - // Find and click logout button (adjust selector based on your UI) + // Find and click logout button const logoutButton = page.locator('button:has-text("Logout"), a:has-text("Logout"), [data-testid="logout"]'); if (await logoutButton.isVisible()) { await logoutButton.click(); - // Should redirect to login await expect(page).toHaveURL(/\/login/); } }); diff --git a/frontend/e2e/theme.spec.ts b/frontend/e2e/theme.spec.ts index 7c413323..16104862 100644 --- a/frontend/e2e/theme.spec.ts +++ b/frontend/e2e/theme.spec.ts @@ -1,11 +1,18 @@ import { test, expect } from '@playwright/test'; test.describe('Theme', () => { + test.beforeEach(async ({ page }) => { + // Clear theme storage before each test + await page.goto('/login'); + await page.evaluate(() => { + localStorage.removeItem('app-theme'); + }); + }); + test('auto theme follows system light preference', async ({ page }) => { await page.emulateMedia({ colorScheme: 'light' }); await page.goto('/login'); - await page.evaluate(() => localStorage.removeItem('app-theme')); - await page.reload(); + await page.waitForLoadState('networkidle'); const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') @@ -16,8 +23,7 @@ test.describe('Theme', () => { test('auto theme follows system dark preference', async ({ page }) => { await page.emulateMedia({ colorScheme: 'dark' }); await page.goto('/login'); - await page.evaluate(() => localStorage.removeItem('app-theme')); - await page.reload(); + await page.waitForLoadState('networkidle'); const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') @@ -30,6 +36,7 @@ test.describe('Theme', () => { await page.goto('/login'); await page.evaluate(() => localStorage.setItem('app-theme', 'dark')); await page.reload(); + await page.waitForLoadState('networkidle'); const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') @@ -42,6 +49,7 @@ test.describe('Theme', () => { await page.goto('/login'); await page.evaluate(() => localStorage.setItem('app-theme', 'light')); await page.reload(); + await page.waitForLoadState('networkidle'); const hasDarkClass = await page.evaluate(() => document.documentElement.classList.contains('dark') @@ -53,6 +61,7 @@ test.describe('Theme', () => { await page.goto('/login'); await page.evaluate(() => localStorage.setItem('app-theme', 'dark')); await page.goto('/register'); + await page.waitForLoadState('networkidle'); const storedTheme = await page.evaluate(() => localStorage.getItem('app-theme')); expect(storedTheme).toBe('dark'); From 18a29ee34945b96cb08104670e68f02db31c12aa Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 01:13:01 +0100 Subject: [PATCH 11/32] ci - removed backend unit tests job (will add later) - tests fix --- .github/workflows/ci.yml | 33 ++-------------------------- frontend/e2e/auth.spec.ts | 46 ++++++++++++++------------------------- 2 files changed, 18 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff87a361..e528f130 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,36 +43,8 @@ jobs: name: frontend-coverage path: frontend/coverage/ - backend-unit: - name: Backend Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Set up uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - cache-dependency-glob: "backend/uv.lock" - - - name: Install Python dependencies - run: | - cd backend - uv python install 3.12 - uv sync --frozen - - - name: Run unit tests - timeout-minutes: 5 - run: | - cd backend - uv run pytest tests/unit -v --cov=app --cov-branch --cov-report=xml --cov-report=term - - - name: Upload coverage - uses: actions/upload-artifact@v6 - if: always() - with: - name: backend-unit-coverage - path: backend/coverage.xml + # NOTE: Backend has no pure unit tests (all tests require MongoDB/Redis) + # All backend tests run in backend-integration job below # ============================================================ # Phase 2: Integration/E2E tests run in parallel after unit tests @@ -196,7 +168,6 @@ jobs: backend-integration: name: Backend Integration Tests - needs: backend-unit runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index 73e89db9..75d456a9 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -2,18 +2,19 @@ import { test, expect } from '@playwright/test'; test.describe('Authentication', () => { test.beforeEach(async ({ page }) => { - // Clear all storage (localStorage stores auth state, not cookies) + // Clear ALL auth state: cookies (HTTP-only auth token) + localStorage (cached state) + await page.context().clearCookies(); await page.goto('/login'); await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }); - await page.reload(); }); test('shows login page with form elements', async ({ page }) => { await page.goto('/login'); - await page.waitForLoadState('networkidle'); + // Wait for the login form to render + await page.waitForSelector('#username'); await expect(page.locator('h2')).toContainText('Sign in to your account'); await expect(page.locator('#username')).toBeVisible(); @@ -23,7 +24,7 @@ test.describe('Authentication', () => { test('shows validation when submitting empty form', async ({ page }) => { await page.goto('/login'); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('#username'); const usernameInput = page.locator('#username'); await expect(usernameInput).toHaveAttribute('required', ''); @@ -31,19 +32,18 @@ test.describe('Authentication', () => { test('shows error with invalid credentials', async ({ page }) => { await page.goto('/login'); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('#username'); await page.fill('#username', 'invaliduser'); await page.fill('#password', 'wrongpassword'); await page.click('button[type="submit"]'); - // Wait for error message to appear await expect(page.locator('p.text-red-600, p.text-red-400')).toBeVisible(); }); test('redirects to editor on successful login', async ({ page }) => { await page.goto('/login'); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('#username'); await page.fill('#username', 'user'); await page.fill('#password', 'user123'); @@ -54,7 +54,7 @@ test.describe('Authentication', () => { test('shows loading state during login', async ({ page }) => { await page.goto('/login'); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('#username'); await page.fill('#username', 'user'); await page.fill('#password', 'user123'); @@ -62,33 +62,20 @@ test.describe('Authentication', () => { const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); - // Button should show loading text or sign in await expect(submitButton).toContainText(/Logging in|Sign in/); }); test('redirects unauthenticated users from protected routes', async ({ page }) => { - // Ensure clean state - await page.goto('/login'); - await page.evaluate(() => localStorage.clear()); - - // Try to access protected route await page.goto('/editor'); - await page.waitForLoadState('networkidle'); - - // Should redirect to login + // Should redirect to login and show login form + await page.waitForSelector('#username'); await expect(page).toHaveURL(/\/login/); }); test('preserves redirect path after login', async ({ page }) => { - // Ensure clean state - await page.goto('/login'); - await page.evaluate(() => localStorage.clear()); - - // Try to access specific protected route await page.goto('/settings'); - await page.waitForLoadState('networkidle'); - // Should redirect to login + await page.waitForSelector('#username'); await expect(page).toHaveURL(/\/login/); // Login @@ -96,13 +83,12 @@ test.describe('Authentication', () => { await page.fill('#password', 'user123'); await page.click('button[type="submit"]'); - // Should redirect back to settings await expect(page).toHaveURL(/\/settings/); }); test('has link to registration page', async ({ page }) => { await page.goto('/login'); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('#username'); const registerLink = page.locator('a[href="/register"]'); await expect(registerLink).toBeVisible(); @@ -111,7 +97,7 @@ test.describe('Authentication', () => { test('can navigate to registration page', async ({ page }) => { await page.goto('/login'); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('#username'); await page.click('a[href="/register"]'); @@ -121,14 +107,14 @@ test.describe('Authentication', () => { test.describe('Logout', () => { test.beforeEach(async ({ page }) => { - // Clear state first + // Clear all state first + await page.context().clearCookies(); await page.goto('/login'); await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }); - await page.reload(); - await page.waitForLoadState('networkidle'); + await page.waitForSelector('#username'); // Login await page.fill('#username', 'user'); From 3750c6f83e4597fcb86ca4245cbecb25e4de901d Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 01:39:52 +0100 Subject: [PATCH 12/32] ci fixes --- .github/workflows/ci.yml | 4 ++++ frontend/playwright.config.ts | 8 ++++---- frontend/rollup.config.js | 15 +++++++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e528f130..6481b1da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,10 @@ jobs: curl --retry 60 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:443/api/v1/health/live echo "Waiting for frontend..." curl --retry 60 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:5001/ + echo "Testing frontend->backend proxy..." + docker exec frontend curl -ksf https://backend:443/api/v1/health/live || echo "WARNING: Frontend cannot reach backend!" + echo "Testing auth endpoint via proxy..." + curl -ksf https://127.0.0.1:5001/api/v1/auth/verify-token || echo "WARNING: Auth endpoint returned error (expected without cookies)" echo "Services ready!" - name: Run E2E tests diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index f88b8f66..452b5304 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -4,11 +4,11 @@ export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 1 : 0, // Reduced: 1 retry is enough to catch flakes - workers: process.env.CI ? 2 : undefined, // Increased: tests are independent - timeout: 30000, // 30s is plenty for page operations + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 2 : undefined, + timeout: 10000, // 10s max per test expect: { - timeout: 5000, // 5s for element expectations + timeout: 3000, // 3s for assertions }, reporter: process.env.CI ? [['html'], ['github']] : 'html', use: { diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js index 4a19f998..1f1f8415 100644 --- a/frontend/rollup.config.js +++ b/frontend/rollup.config.js @@ -62,10 +62,21 @@ function startServer() { proxyRes.pipe(res, { end: true }); }); + // Socket timeout prevents hanging when backend is unreachable + proxyReq.on('socket', (socket) => { + socket.setTimeout(2000); + socket.on('timeout', () => { + console.error('Proxy socket timeout - backend unreachable'); + proxyReq.destroy(new Error('Socket timeout')); + }); + }); + proxyReq.on('error', (e) => { console.error(`Proxy request error: ${e.message}`); - res.writeHead(502); - res.end('Bad Gateway'); + if (!res.headersSent) { + res.writeHead(502); + res.end('Bad Gateway'); + } }); req.pipe(proxyReq, { end: true }); From bb400485060220fca74ccaf764536f82aac76071 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 01:54:02 +0100 Subject: [PATCH 13/32] base url fix + tests fix (rabbit hints) --- frontend/e2e/auth.spec.ts | 30 ++++++++++++++++++++++-------- frontend/src/main.ts | 5 +++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index 75d456a9..cccd21fc 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -22,12 +22,27 @@ test.describe('Authentication', () => { await expect(page.locator('button[type="submit"]')).toBeVisible(); }); - test('shows validation when submitting empty form', async ({ page }) => { + test('prevents submission and shows validation for empty form', async ({ page }) => { await page.goto('/login'); await page.waitForSelector('#username'); + // Click submit without filling any fields + await page.click('button[type="submit"]'); + + // Form should not submit - still on login page + await expect(page).toHaveURL(/\/login/); + + // Browser focuses first invalid required field and shows validation const usernameInput = page.locator('#username'); - await expect(usernameInput).toHaveAttribute('required', ''); + await expect(usernameInput).toBeFocused(); + + // Check HTML5 validity state + const isInvalid = await usernameInput.evaluate((el: HTMLInputElement) => !el.validity.valid); + expect(isInvalid).toBe(true); + + // Verify validation message exists (browser shows "Please fill out this field" or similar) + const validationMessage = await usernameInput.evaluate((el: HTMLInputElement) => el.validationMessage); + expect(validationMessage.length).toBeGreaterThan(0); }); test('shows error with invalid credentials', async ({ page }) => { @@ -124,12 +139,11 @@ test.describe('Logout', () => { }); test('can logout from authenticated state', async ({ page }) => { - // Find and click logout button - const logoutButton = page.locator('button:has-text("Logout"), a:has-text("Logout"), [data-testid="logout"]'); + // Logout button is in the header - must exist when authenticated + const logoutButton = page.locator('button:has-text("Logout")').first(); - if (await logoutButton.isVisible()) { - await logoutButton.click(); - await expect(page).toHaveURL(/\/login/); - } + await expect(logoutButton).toBeVisible(); + await logoutButton.click(); + await expect(page).toHaveURL(/\/login/); }); }); diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 204f624b..dea68f48 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,9 +3,10 @@ import { mount } from 'svelte'; import App from './App.svelte'; import './app.css'; -// Configure the API client with base URL and credentials +// Configure the API client with credentials +// Note: SDK already has full paths like '/api/v1/auth/login', so baseUrl should be empty client.setConfig({ - baseUrl: '/api/v1', + baseUrl: '', credentials: 'include', }); From 3cbd7d714c6d055d8bbdc05c4d07e0086b7abe50 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 02:06:30 +0100 Subject: [PATCH 14/32] ci more debug output --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6481b1da..cd8a1880 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,6 +142,17 @@ jobs: curl -ksf https://127.0.0.1:5001/api/v1/auth/verify-token || echo "WARNING: Auth endpoint returned error (expected without cookies)" echo "Services ready!" + - name: Debug frontend state + run: | + echo "=== Check JS bundle for baseUrl ===" + curl -ks https://127.0.0.1:5001/build/main.js | grep -o "baseUrl[^,]*" | head -3 || echo "not found" + echo "" + echo "=== Test auth API directly ===" + curl -ksv https://127.0.0.1:5001/api/v1/auth/verify-token 2>&1 | head -30 + echo "" + echo "=== Frontend container logs ===" + docker logs frontend 2>&1 | tail -50 + - name: Run E2E tests working-directory: frontend env: From 5921f94030a30640f592efc9b01dc04809c69c87 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 02:19:44 +0100 Subject: [PATCH 15/32] ci more debug output --- .github/workflows/ci.yml | 12 +++++------- frontend/playwright.config.ts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd8a1880..9717b75b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,14 +144,12 @@ jobs: - name: Debug frontend state run: | - echo "=== Check JS bundle for baseUrl ===" - curl -ks https://127.0.0.1:5001/build/main.js | grep -o "baseUrl[^,]*" | head -3 || echo "not found" + echo "=== Test auth API - show response body and status ===" + curl -ks -w "\nHTTP_CODE:%{http_code}\n" https://127.0.0.1:5001/api/v1/auth/verify-token echo "" - echo "=== Test auth API directly ===" - curl -ksv https://127.0.0.1:5001/api/v1/auth/verify-token 2>&1 | head -30 - echo "" - echo "=== Frontend container logs ===" - docker logs frontend 2>&1 | tail -50 + echo "=== Check if app renders (wait 5s for JS) ===" + sleep 5 + curl -ks https://127.0.0.1:5001/login | grep -E "(username|password|Sign in|spinner|loading)" | head -10 || echo "No form elements found" - name: Run E2E tests working-directory: frontend diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 452b5304..4a60cea7 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ use: { baseURL: 'https://localhost:5001', ignoreHTTPSErrors: true, - trace: 'on-first-retry', + trace: 'on', screenshot: 'only-on-failure', }, projects: [ From fa55c9c4d4cc7f8f01091abeb6a216a5134025ed Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 02:42:54 +0100 Subject: [PATCH 16/32] error display page + fixes --- frontend/src/App.svelte | 11 ++- frontend/src/components/ErrorDisplay.svelte | 72 +++++++++++++++++++ frontend/src/main.ts | 34 ++++++++- .../__tests__/notificationStore.test.ts | 42 ++++++----- frontend/src/stores/errorStore.ts | 26 +++++++ 5 files changed, 162 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/ErrorDisplay.svelte create mode 100644 frontend/src/stores/errorStore.ts diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 5dd89302..7cb7be7e 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -18,15 +18,22 @@ import ToastContainer from "./components/ToastContainer.svelte"; import ProtectedRoute from "./components/ProtectedRoute.svelte"; import Spinner from "./components/Spinner.svelte"; + import ErrorDisplay from "./components/ErrorDisplay.svelte"; import { theme } from './stores/theme'; import { initializeAuth } from './lib/auth-init'; + import { appError } from './stores/errorStore'; // Theme value derived from store with proper cleanup let themeValue = $state('auto'); const unsubscribeTheme = theme.subscribe(value => { themeValue = value; }); + // Global error state + let globalError = $state<{ error: Error | string; title?: string } | null>(null); + const unsubscribeError = appError.subscribe(value => { globalError = value; }); + onDestroy(() => { unsubscribeTheme(); + unsubscribeError(); }); let authInitialized = $state(false); @@ -186,7 +193,9 @@ {/snippet} -{#if !authInitialized} +{#if globalError} + +{:else if !authInitialized}

diff --git a/frontend/src/components/ErrorDisplay.svelte b/frontend/src/components/ErrorDisplay.svelte new file mode 100644 index 00000000..c64b6238 --- /dev/null +++ b/frontend/src/components/ErrorDisplay.svelte @@ -0,0 +1,72 @@ + + +
+
+ +
+
+ + + +
+
+ + +

+ {title} +

+ + +

+ {errorMessage} +

+ + + {#if showDetails && errorStack} +
+ + Show technical details + +
{errorStack}
+
+ {/if} + + +
+ + +
+ + +

+ If this problem persists, please check the browser console for more details. +

+
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts index dea68f48..49dfcf01 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,6 +1,8 @@ import { client } from './lib/api/client.gen'; import { mount } from 'svelte'; import App from './App.svelte'; +import ErrorDisplay from './components/ErrorDisplay.svelte'; +import { appError } from './stores/errorStore'; import './app.css'; // Configure the API client with credentials @@ -10,8 +12,34 @@ client.setConfig({ credentials: 'include', }); -const app = mount(App, { - target: document.body, -}); +// Global error handlers to catch unhandled errors +window.onerror = (message, source, lineno, colno, error) => { + console.error('[Global Error]', { message, source, lineno, colno, error }); + appError.setError(error || String(message), 'Unexpected Error'); + return true; // Prevent default browser error handling +}; + +window.onunhandledrejection = (event) => { + console.error('[Unhandled Promise Rejection]', event.reason); + appError.setError(event.reason, 'Unhandled Promise Error'); +}; + +// Mount the app with error handling +let app; +try { + app = mount(App, { + target: document.body, + }); +} catch (error) { + console.error('[Mount Error]', error); + // If App fails to mount, show error display directly + app = mount(ErrorDisplay, { + target: document.body, + props: { + error: error instanceof Error ? error : new Error(String(error)), + title: 'Failed to Load Application' + } + }); +} export default app; diff --git a/frontend/src/stores/__tests__/notificationStore.test.ts b/frontend/src/stores/__tests__/notificationStore.test.ts index 81a2bfe1..d45e595f 100644 --- a/frontend/src/stores/__tests__/notificationStore.test.ts +++ b/frontend/src/stores/__tests__/notificationStore.test.ts @@ -14,13 +14,17 @@ vi.mock('../../lib/api', () => ({ deleteNotificationApiV1NotificationsNotificationIdDelete: (...args: unknown[]) => mockDeleteNotification(...args), })); -const createMockNotification = (overrides = {}) => ({ +const createMockNotification = (overrides: Record = {}) => ({ notification_id: `notif-${Math.random().toString(36).slice(2)}`, - title: 'Test Notification', - message: 'Test message', - status: 'unread' as const, + channel: 'in_app' as const, + status: 'pending' as const, + subject: 'Test Notification', + body: 'Test message body', + action_url: null, created_at: new Date().toISOString(), - tags: [], + read_at: null, + severity: 'medium' as const, + tags: [] as string[], ...overrides, }); @@ -74,8 +78,8 @@ describe('notificationStore', () => { it('populates notifications on success', async () => { const notifications = [ - createMockNotification({ notification_id: 'n1', title: 'First' }), - createMockNotification({ notification_id: 'n2', title: 'Second' }), + createMockNotification({ notification_id: 'n1', subject: 'First' }), + createMockNotification({ notification_id: 'n2', subject: 'Second' }), ]; mockGetNotifications.mockResolvedValue({ data: { notifications }, @@ -86,7 +90,7 @@ describe('notificationStore', () => { await notificationStore.load(); expect(get(notificationStore).notifications).toHaveLength(2); - expect(get(notificationStore).notifications[0].title).toBe('First'); + expect(get(notificationStore).notifications[0].subject).toBe('First'); }); it('sets loading false after success', async () => { @@ -186,7 +190,7 @@ describe('notificationStore', () => { const { notificationStore } = await import('../notificationStore'); await notificationStore.load(); - const newNotification = createMockNotification({ notification_id: 'new', title: 'New' }); + const newNotification = createMockNotification({ notification_id: 'new', subject: 'New' }); notificationStore.add(newNotification); const notifications = get(notificationStore).notifications; @@ -218,7 +222,7 @@ describe('notificationStore', () => { mockGetNotifications.mockResolvedValue({ data: { notifications: [ - createMockNotification({ notification_id: 'n1', status: 'unread' }), + createMockNotification({ notification_id: 'n1', status: 'pending' }), ], }, error: null, @@ -247,7 +251,7 @@ describe('notificationStore', () => { const result = await notificationStore.markAsRead('n1'); expect(result).toBe(false); - expect(get(notificationStore).notifications[0].status).toBe('unread'); + expect(get(notificationStore).notifications[0].status).toBe('pending'); }); it('calls API with correct notification ID', async () => { @@ -267,8 +271,8 @@ describe('notificationStore', () => { mockGetNotifications.mockResolvedValue({ data: { notifications: [ - createMockNotification({ notification_id: 'n1', status: 'unread' }), - createMockNotification({ notification_id: 'n2', status: 'unread' }), + createMockNotification({ notification_id: 'n1', status: 'pending' }), + createMockNotification({ notification_id: 'n2', status: 'pending' }), ], }, error: null, @@ -390,9 +394,9 @@ describe('notificationStore', () => { mockGetNotifications.mockResolvedValue({ data: { notifications: [ - createMockNotification({ notification_id: 'n1', status: 'unread' }), + createMockNotification({ notification_id: 'n1', status: 'pending' }), createMockNotification({ notification_id: 'n2', status: 'read' }), - createMockNotification({ notification_id: 'n3', status: 'unread' }), + createMockNotification({ notification_id: 'n3', status: 'pending' }), ], }, error: null, @@ -424,7 +428,7 @@ describe('notificationStore', () => { mockGetNotifications.mockResolvedValue({ data: { notifications: [ - createMockNotification({ notification_id: 'n1', status: 'unread' }), + createMockNotification({ notification_id: 'n1', status: 'pending' }), ], }, error: null, @@ -443,8 +447,8 @@ describe('notificationStore', () => { describe('notifications derived store', () => { it('exposes notifications array', async () => { const notifs = [ - createMockNotification({ notification_id: 'n1', title: 'First' }), - createMockNotification({ notification_id: 'n2', title: 'Second' }), + createMockNotification({ notification_id: 'n1', subject: 'First' }), + createMockNotification({ notification_id: 'n2', subject: 'Second' }), ]; mockGetNotifications.mockResolvedValue({ data: { notifications: notifs }, @@ -455,7 +459,7 @@ describe('notificationStore', () => { await notificationStore.load(); expect(get(notifications)).toHaveLength(2); - expect(get(notifications)[0].title).toBe('First'); + expect(get(notifications)[0].subject).toBe('First'); }); }); }); diff --git a/frontend/src/stores/errorStore.ts b/frontend/src/stores/errorStore.ts new file mode 100644 index 00000000..67a10bcb --- /dev/null +++ b/frontend/src/stores/errorStore.ts @@ -0,0 +1,26 @@ +import { writable } from 'svelte/store'; + +export interface AppError { + error: Error | string; + title?: string; + timestamp: number; +} + +function createErrorStore() { + const { subscribe, set, update } = writable(null); + + return { + subscribe, + setError: (error: Error | string, title?: string) => { + console.error('[ErrorStore]', title || 'Error:', error); + set({ + error, + title, + timestamp: Date.now() + }); + }, + clear: () => set(null) + }; +} + +export const appError = createErrorStore(); From 9b21b390f1674dcc85ab02053bb2d4a05a836b79 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 14:15:07 +0100 Subject: [PATCH 17/32] ci playwright fixes --- .github/actions/setup-ci-compose/action.yml | 3 +- frontend/e2e/auth.spec.ts | 24 ++- frontend/rollup.config.js | 7 +- frontend/src/App.svelte | 227 ++++++-------------- frontend/src/routes/Login.svelte | 10 +- 5 files changed, 92 insertions(+), 179 deletions(-) diff --git a/.github/actions/setup-ci-compose/action.yml b/.github/actions/setup-ci-compose/action.yml index 97aec7eb..7ca7ccc9 100644 --- a/.github/actions/setup-ci-compose/action.yml +++ b/.github/actions/setup-ci-compose/action.yml @@ -28,11 +28,12 @@ runs: yq eval '.services.backend.environment += ["MONGO_ROOT_PASSWORD=rootpassword"]' -i docker-compose.ci.yaml yq eval '.services.backend.environment += ["OTEL_SDK_DISABLED=true"]' -i docker-compose.ci.yaml - # Remove hot-reload volume mounts (causes permission issues in CI) + # Remove hot-reload volume mounts (causes permission issues and slow rebuilds in CI) yq eval '.services.backend.volumes = [.services.backend.volumes[] | select(. != "./backend:/app")]' -i docker-compose.ci.yaml yq eval '.services."k8s-worker".volumes = [.services."k8s-worker".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml yq eval '.services."pod-monitor".volumes = [.services."pod-monitor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml yq eval '.services."result-processor".volumes = [.services."result-processor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml + yq eval '.services.frontend.volumes = [.services.frontend.volumes[] | select(. != "./frontend:/app")]' -i docker-compose.ci.yaml # Disable Kafka SASL authentication for CI yq eval 'del(.services.kafka.environment.KAFKA_OPTS)' -i docker-compose.ci.yaml diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index cccd21fc..97323320 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -16,7 +16,7 @@ test.describe('Authentication', () => { // Wait for the login form to render await page.waitForSelector('#username'); - await expect(page.locator('h2')).toContainText('Sign in to your account'); + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); await expect(page.locator('#username')).toBeVisible(); await expect(page.locator('#password')).toBeVisible(); await expect(page.locator('button[type="submit"]')).toBeVisible(); @@ -64,6 +64,8 @@ test.describe('Authentication', () => { await page.fill('#password', 'user123'); await page.click('button[type="submit"]'); + // Wait for Editor page content (router updates DOM before URL) + await expect(page.getByRole('heading', { name: 'Code Editor' })).toBeVisible(); await expect(page).toHaveURL(/\/editor/); }); @@ -98,6 +100,8 @@ test.describe('Authentication', () => { await page.fill('#password', 'user123'); await page.click('button[type="submit"]'); + // Wait for Settings page content (redirect target) + await expect(page.getByRole('heading', { name: 'Settings', level: 1 })).toBeVisible(); await expect(page).toHaveURL(/\/settings/); }); @@ -105,16 +109,17 @@ test.describe('Authentication', () => { await page.goto('/login'); await page.waitForSelector('#username'); - const registerLink = page.locator('a[href="/register"]'); + // Use specific text to avoid matching the Register button in header + const registerLink = page.getByRole('link', { name: 'create a new account' }); await expect(registerLink).toBeVisible(); - await expect(registerLink).toContainText('create a new account'); }); test('can navigate to registration page', async ({ page }) => { await page.goto('/login'); await page.waitForSelector('#username'); - await page.click('a[href="/register"]'); + // Click the specific link in the form, not the header button + await page.getByRole('link', { name: 'create a new account' }).click(); await expect(page).toHaveURL(/\/register/); }); @@ -135,13 +140,18 @@ test.describe('Logout', () => { await page.fill('#username', 'user'); await page.fill('#password', 'user123'); await page.click('button[type="submit"]'); - await expect(page).toHaveURL(/\/editor/); + // Wait for Editor page content (router updates DOM before URL) + await expect(page.getByRole('heading', { name: 'Code Editor' })).toBeVisible(); }); test('can logout from authenticated state', async ({ page }) => { - // Logout button is in the header - must exist when authenticated - const logoutButton = page.locator('button:has-text("Logout")').first(); + // Open user dropdown (contains the logout button) + const userDropdown = page.locator('.user-dropdown-container button').first(); + await expect(userDropdown).toBeVisible(); + await userDropdown.click(); + // Click logout button inside the dropdown + const logoutButton = page.locator('button:has-text("Logout")').first(); await expect(logoutButton).toBeVisible(); await logoutButton.click(); await expect(page).toHaveURL(/\/login/); diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js index 1f1f8415..dbd43129 100644 --- a/frontend/rollup.config.js +++ b/frontend/rollup.config.js @@ -38,7 +38,9 @@ function startServer() { const proxyAgent = new https.Agent({ ca: fs.readFileSync(caPath), - rejectUnauthorized: false // Accept self-signed certificates in development + rejectUnauthorized: false, // Accept self-signed certificates in development + keepAlive: true, // Reuse connections to avoid TLS handshake per request + keepAliveMsecs: 1000 }); server = https.createServer(httpsOptions, (req, res) => { @@ -201,6 +203,7 @@ export default { }) ], watch: { - clearScreen: false + clearScreen: false, + exclude: ['node_modules/**', 'public/build/**', 'test-results/**', 'playwright-report/**', 'e2e/**'] } }; \ No newline at end of file diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 7cb7be7e..64c63a76 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,27 +1,29 @@ - -{#snippet layoutWrapper(content: Snippet, isProtected: boolean = false, isFullWidth: boolean = false)} - {#if isProtected} - -
-
-
- -
- {@render content()} -
-
-
-
-
- {:else} -
-
-
- -
- {@render content()} -
-
-
-
- {/if} -{/snippet} - - -{#snippet homeSnippet()} - {@render layoutWrapper(homeContent, false)} -{/snippet} - -{#snippet homeContent()} - -{/snippet} - -{#snippet loginSnippet()} - {@render layoutWrapper(loginContent, false)} -{/snippet} - -{#snippet loginContent()} - -{/snippet} - -{#snippet registerSnippet()} - {@render layoutWrapper(registerContent, false)} -{/snippet} - -{#snippet registerContent()} - -{/snippet} - -{#snippet privacySnippet()} - {@render layoutWrapper(privacyContent, false)} -{/snippet} - -{#snippet privacyContent()} - -{/snippet} - - -{#snippet editorSnippet()} - {@render layoutWrapper(editorContent, true)} -{/snippet} - -{#snippet editorContent()} - -{/snippet} - -{#snippet settingsSnippet()} - {@render layoutWrapper(settingsContent, true)} -{/snippet} - -{#snippet settingsContent()} - -{/snippet} - -{#snippet notificationsSnippet()} - {@render layoutWrapper(notificationsContent, true)} -{/snippet} - -{#snippet notificationsContent()} - -{/snippet} - - -{#snippet adminEventsSnippet()} - {@render layoutWrapper(adminEventsContent, true, true)} -{/snippet} - -{#snippet adminEventsContent()} - -{/snippet} - -{#snippet adminSagasSnippet()} - {@render layoutWrapper(adminSagasContent, true, true)} -{/snippet} - -{#snippet adminSagasContent()} - -{/snippet} - -{#snippet adminUsersSnippet()} - {@render layoutWrapper(adminUsersContent, true, true)} -{/snippet} - -{#snippet adminUsersContent()} - -{/snippet} - -{#snippet adminSettingsSnippet()} - {@render layoutWrapper(adminSettingsContent, true, true)} -{/snippet} - -{#snippet adminSettingsContent()} - -{/snippet} - {#if globalError} -{:else if !authInitialized} -
- -
{:else} - +
+
+
+ +
+ {#if !authInitialized} +
+ +
+ {:else} + + {/if} +
+
+
+
{/if} - - diff --git a/frontend/src/routes/Login.svelte b/frontend/src/routes/Login.svelte index 28d33c1e..030d9500 100644 --- a/frontend/src/routes/Login.svelte +++ b/frontend/src/routes/Login.svelte @@ -29,12 +29,12 @@ error = null; // Clear previous error try { await login(username, password); - - // Load and apply user settings (theme, etc) - await loadUserSettings(); - + + // Load user settings in background (non-blocking) + loadUserSettings().catch(err => console.warn('Failed to load user settings:', err)); + addToast("Login successful! Welcome back.", "success"); - + // Check if there's a saved redirect path const redirectPath = sessionStorage.getItem('redirectAfterLogin'); if (redirectPath) { From 143214bec0424091483e9b1c723b99dbb4bc197b Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 14:26:24 +0100 Subject: [PATCH 18/32] ci: removed debug step, added users seeding step --- .github/workflows/ci.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9717b75b..5d32df37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,14 +142,10 @@ jobs: curl -ksf https://127.0.0.1:5001/api/v1/auth/verify-token || echo "WARNING: Auth endpoint returned error (expected without cookies)" echo "Services ready!" - - name: Debug frontend state + - name: Seed test users run: | - echo "=== Test auth API - show response body and status ===" - curl -ks -w "\nHTTP_CODE:%{http_code}\n" https://127.0.0.1:5001/api/v1/auth/verify-token - echo "" - echo "=== Check if app renders (wait 5s for JS) ===" - sleep 5 - curl -ks https://127.0.0.1:5001/login | grep -E "(username|password|Sign in|spinner|loading)" | head -10 || echo "No form elements found" + docker compose -f docker-compose.ci.yaml exec -T backend uv run python scripts/seed_users.py + echo "Test users seeded" - name: Run E2E tests working-directory: frontend From aa277427ec1262c376d31584b3eed19c755d2ae8 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 15:12:33 +0100 Subject: [PATCH 19/32] seed users fix (reapplying password) --- backend/scripts/seed_users.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/scripts/seed_users.py b/backend/scripts/seed_users.py index dcb86e2b..85d1abee 100755 --- a/backend/scripts/seed_users.py +++ b/backend/scripts/seed_users.py @@ -35,10 +35,11 @@ async def upsert_user( existing = await db.users.find_one({"username": username}) if existing: - print(f"User '{username}' exists - updating role={role}, is_superuser={is_superuser}") + print(f"User '{username}' exists - updating password, role={role}, is_superuser={is_superuser}") await db.users.update_one( {"username": username}, {"$set": { + "hashed_password": pwd_context.hash(password), "role": role, "is_superuser": is_superuser, "is_active": True, From f6c1dd0b128e1f8f71823b22c65bac5f32874d2d Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 15:28:04 +0100 Subject: [PATCH 20/32] db name fixes (no more suffix) --- backend/app/core/providers.py | 2 +- backend/app/services/coordinator/coordinator.py | 2 +- backend/app/services/k8s_worker/worker.py | 2 +- backend/scripts/seed_users.py | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/app/core/providers.py b/backend/app/core/providers.py index 47cdbde3..843c3d7c 100644 --- a/backend/app/core/providers.py +++ b/backend/app/core/providers.py @@ -90,7 +90,7 @@ class DatabaseProvider(Provider): async def get_database_connection(self, settings: Settings) -> AsyncIterator[AsyncDatabaseConnection]: db_config = DatabaseConfig( mongodb_url=settings.MONGODB_URL, - db_name=settings.PROJECT_NAME + "_test" if settings.TESTING else settings.PROJECT_NAME, + db_name=settings.PROJECT_NAME, server_selection_timeout_ms=5000, connect_timeout_ms=5000, max_pool_size=50, diff --git a/backend/app/services/coordinator/coordinator.py b/backend/app/services/coordinator/coordinator.py index 8005f168..7a99493b 100644 --- a/backend/app/services/coordinator/coordinator.py +++ b/backend/app/services/coordinator/coordinator.py @@ -554,7 +554,7 @@ async def run_coordinator() -> None: tz_aware=True, serverSelectionTimeoutMS=5000 ) - db_name = settings.PROJECT_NAME + "_test" if settings.TESTING else settings.PROJECT_NAME + db_name = settings.PROJECT_NAME database = db_client[db_name] await SchemaManager(database).apply_all() diff --git a/backend/app/services/k8s_worker/worker.py b/backend/app/services/k8s_worker/worker.py index bed572e4..a8327c92 100644 --- a/backend/app/services/k8s_worker/worker.py +++ b/backend/app/services/k8s_worker/worker.py @@ -561,7 +561,7 @@ async def run_kubernetes_worker() -> None: tz_aware=True, serverSelectionTimeoutMS=5000 ) - db_name = settings.PROJECT_NAME + "_test" if settings.TESTING else settings.PROJECT_NAME + db_name = settings.PROJECT_NAME database = db_client[db_name] await db_client.admin.command("ping") logger.info(f"Connected to database: {db_name}") diff --git a/backend/scripts/seed_users.py b/backend/scripts/seed_users.py index 85d1abee..9425d2e3 100755 --- a/backend/scripts/seed_users.py +++ b/backend/scripts/seed_users.py @@ -64,10 +64,11 @@ async def upsert_user( async def seed_users() -> None: mongodb_url = os.getenv("MONGODB_URL", "mongodb://mongo:27017/integr8scode") - print("Connecting to MongoDB...") + db_name = os.getenv("PROJECT_NAME", "integr8scode") + + print(f"Connecting to MongoDB (database: {db_name})...") client: AsyncIOMotorClient = AsyncIOMotorClient(mongodb_url) - db_name = mongodb_url.rstrip("/").split("/")[-1].split("?")[0] or "integr8scode" db = client[db_name] # Default user From 38576decd6f52357e0fbbbf068d4940b38fa3ad6 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Mon, 22 Dec 2025 16:34:40 +0100 Subject: [PATCH 21/32] error display fix + moved from project name to db name for database --- backend/app/core/providers.py | 2 +- .../app/services/coordinator/coordinator.py | 2 +- backend/app/services/k8s_worker/worker.py | 2 +- backend/app/settings.py | 1 + backend/scripts/seed_users.py | 2 +- backend/workers/dlq_processor.py | 2 +- backend/workers/run_event_replay.py | 2 +- backend/workers/run_saga_orchestrator.py | 2 +- frontend/src/components/ErrorDisplay.svelte | 37 ++++++++++--------- frontend/src/main.ts | 1 + 10 files changed, 28 insertions(+), 25 deletions(-) diff --git a/backend/app/core/providers.py b/backend/app/core/providers.py index 843c3d7c..fbd0b2f2 100644 --- a/backend/app/core/providers.py +++ b/backend/app/core/providers.py @@ -90,7 +90,7 @@ class DatabaseProvider(Provider): async def get_database_connection(self, settings: Settings) -> AsyncIterator[AsyncDatabaseConnection]: db_config = DatabaseConfig( mongodb_url=settings.MONGODB_URL, - db_name=settings.PROJECT_NAME, + db_name=settings.DATABASE_NAME, server_selection_timeout_ms=5000, connect_timeout_ms=5000, max_pool_size=50, diff --git a/backend/app/services/coordinator/coordinator.py b/backend/app/services/coordinator/coordinator.py index 7a99493b..df255568 100644 --- a/backend/app/services/coordinator/coordinator.py +++ b/backend/app/services/coordinator/coordinator.py @@ -554,7 +554,7 @@ async def run_coordinator() -> None: tz_aware=True, serverSelectionTimeoutMS=5000 ) - db_name = settings.PROJECT_NAME + db_name = settings.DATABASE_NAME database = db_client[db_name] await SchemaManager(database).apply_all() diff --git a/backend/app/services/k8s_worker/worker.py b/backend/app/services/k8s_worker/worker.py index a8327c92..cb08e2dd 100644 --- a/backend/app/services/k8s_worker/worker.py +++ b/backend/app/services/k8s_worker/worker.py @@ -561,7 +561,7 @@ async def run_kubernetes_worker() -> None: tz_aware=True, serverSelectionTimeoutMS=5000 ) - db_name = settings.PROJECT_NAME + db_name = settings.DATABASE_NAME database = db_client[db_name] await db_client.admin.command("ping") logger.info(f"Connected to database: {db_name}") diff --git a/backend/app/settings.py b/backend/app/settings.py index 2346bf3c..f0de8967 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -9,6 +9,7 @@ class Settings(BaseSettings): PROJECT_NAME: str = "integr8scode" + DATABASE_NAME: str = "integr8scode_db" API_V1_STR: str = "/api/v1" SECRET_KEY: str = Field( ..., # Actual key be loaded from .env file diff --git a/backend/scripts/seed_users.py b/backend/scripts/seed_users.py index 9425d2e3..d1d1c7ed 100755 --- a/backend/scripts/seed_users.py +++ b/backend/scripts/seed_users.py @@ -64,7 +64,7 @@ async def upsert_user( async def seed_users() -> None: mongodb_url = os.getenv("MONGODB_URL", "mongodb://mongo:27017/integr8scode") - db_name = os.getenv("PROJECT_NAME", "integr8scode") + db_name = os.getenv("DATABASE_NAME", "integr8scode_db") print(f"Connecting to MongoDB (database: {db_name})...") diff --git a/backend/workers/dlq_processor.py b/backend/workers/dlq_processor.py index 0d25d15e..9bf400fd 100644 --- a/backend/workers/dlq_processor.py +++ b/backend/workers/dlq_processor.py @@ -101,7 +101,7 @@ async def main() -> None: tz_aware=True, serverSelectionTimeoutMS=5000, ) - db_name = settings.PROJECT_NAME + "_test" if settings.TESTING else settings.PROJECT_NAME + db_name = settings.DATABASE_NAME database: AsyncIOMotorDatabase = db_client[db_name] await db_client.admin.command("ping") logger.info(f"Connected to database: {db_name}") diff --git a/backend/workers/run_event_replay.py b/backend/workers/run_event_replay.py index 8481fb45..8b2de419 100644 --- a/backend/workers/run_event_replay.py +++ b/backend/workers/run_event_replay.py @@ -40,7 +40,7 @@ async def run_replay_service() -> None: tz_aware=True, serverSelectionTimeoutMS=5000 ) - db_name = settings.PROJECT_NAME + "_test" if settings.TESTING else settings.PROJECT_NAME + db_name = settings.DATABASE_NAME database = db_client[db_name] # Verify connection diff --git a/backend/workers/run_saga_orchestrator.py b/backend/workers/run_saga_orchestrator.py index d57a9650..241a2456 100644 --- a/backend/workers/run_saga_orchestrator.py +++ b/backend/workers/run_saga_orchestrator.py @@ -31,7 +31,7 @@ async def run_saga_orchestrator() -> None: tz_aware=True, serverSelectionTimeoutMS=5000 ) - db_name = settings.PROJECT_NAME + "_test" if settings.TESTING else settings.PROJECT_NAME + db_name = settings.DATABASE_NAME database = db_client[db_name] # Verify connection diff --git a/frontend/src/components/ErrorDisplay.svelte b/frontend/src/components/ErrorDisplay.svelte index c64b6238..964d38f3 100644 --- a/frontend/src/components/ErrorDisplay.svelte +++ b/frontend/src/components/ErrorDisplay.svelte @@ -1,12 +1,23 @@