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 @@
-
+
+
+
+
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 @@
-
-
-
-
-
+
+
-
+
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 @@