diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index d7a01f8c4..2a1c6afee 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -14,12 +14,15 @@ concurrency: jobs: run-tests: uses: ./.github/workflows/run_tests.yml + run-e2e-tests: + uses: ./.github/workflows/e2e_tests.yml build-dev-container: uses: ./.github/workflows/build_docker.yml secrets: inherit deploy-dev-container: needs: - run-tests + - run-e2e-tests - build-dev-container uses: ./.github/workflows/deploy_docker.yml secrets: inherit diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml new file mode 100644 index 000000000..39c2aca70 --- /dev/null +++ b/.github/workflows/e2e_tests.yml @@ -0,0 +1,182 @@ +name: E2E Tests + +on: + workflow_call: + +jobs: + e2e: + runs-on: ubuntu-latest + timeout-minutes: 30 + + services: + postgres: + image: postgres:18.2-alpine + env: + POSTGRES_USER: thesis-management-postgres + POSTGRES_PASSWORD: thesis-management-postgres + POSTGRES_DB: thesis-management + ports: + - 5144:5432 + options: >- + --health-cmd "pg_isready -d thesis-management -U thesis-management-postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref || github.ref }} + fetch-depth: 1 + + # Start Keycloak manually (service containers don't support custom commands) + - name: Start Keycloak + run: | + docker run -d --name keycloak \ + -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \ + -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \ + -p 8081:8080 \ + quay.io/keycloak/keycloak:26.4 \ + start-dev + + - name: Wait for Keycloak to be ready + run: | + echo "Waiting for Keycloak..." + for i in $(seq 1 60); do + if curl -sf http://localhost:8081/realms/master > /dev/null 2>&1; then + echo "Keycloak is ready" + break + fi + if [ "$i" -eq 60 ]; then + echo "Keycloak failed to start" + docker logs keycloak + exit 1 + fi + sleep 2 + done + + - name: Import Keycloak realm + run: | + # Get admin token + TOKEN=$(curl -sf -X POST "http://localhost:8081/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=admin&grant_type=password&client_id=admin-cli" | jq -r '.access_token') + + # Import realm + curl -sf -X POST "http://localhost:8081/admin/realms" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d @keycloak/thesis-management-realm.json + + echo "Realm imported successfully" + + # Set up Java for server + - name: Set up JDK 25 + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'zulu' + cache: 'gradle' + + - name: Grant execute permission for gradlew + run: chmod +x ./server/gradlew + + # Start server in background with dev profile + - name: Start server + working-directory: ./server + run: ./gradlew bootRun --args='--spring.profiles.active=dev' & + env: + SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5144/thesis-management + SPRING_DATASOURCE_USERNAME: thesis-management-postgres + SPRING_DATASOURCE_PASSWORD: thesis-management-postgres + KEYCLOAK_HOST: http://localhost:8081 + KEYCLOAK_REALM_NAME: thesis-management + KEYCLOAK_CLIENT_ID: thesis-management-app + KEYCLOAK_SERVICE_CLIENT_ID: thesis-management-service-client + KEYCLOAK_SERVICE_CLIENT_SECRET: "" + CLIENT_HOST: http://localhost:3000 + + # Set up Node.js for client + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: client/package-lock.json + + - name: Install client dependencies + working-directory: ./client + run: npm ci + + - name: Install Playwright browsers + working-directory: ./client + run: npx playwright install --with-deps chromium + + # Start client dev server in background + - name: Start client dev server + working-directory: ./client + run: npx webpack serve --env NODE_ENV=development & + env: + SERVER_HOST: http://localhost:8080 + KEYCLOAK_HOST: http://localhost:8081 + KEYCLOAK_REALM_NAME: thesis-management + KEYCLOAK_CLIENT_ID: thesis-management-app + + # Wait for both services to be ready + - name: Wait for server to be ready + run: | + echo "Waiting for Spring Boot server..." + for i in $(seq 1 120); do + if curl -sf http://localhost:8080/api/actuator/health > /dev/null 2>&1; then + echo "Server is ready" + break + fi + if [ "$i" -eq 120 ]; then + echo "Server failed to start" + exit 1 + fi + sleep 2 + done + + - name: Wait for client to be ready + run: | + echo "Waiting for client dev server..." + for i in $(seq 1 60); do + if curl -sf http://localhost:3000 > /dev/null 2>&1; then + echo "Client is ready" + break + fi + if [ "$i" -eq 60 ]; then + echo "Client failed to start" + exit 1 + fi + sleep 2 + done + + # Run E2E tests + - name: Run Playwright tests + working-directory: ./client + run: npx playwright test + env: + CI: "1" + CLIENT_URL: http://localhost:3000 + KEYCLOAK_HOST: http://localhost:8081 + KEYCLOAK_REALM_NAME: thesis-management + KEYCLOAK_CLIENT_ID: thesis-management-app + + - name: Upload Playwright report + uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: client/playwright-report/ + retention-days: 14 + + - name: Upload test results + uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: playwright-results + path: client/test-results/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 8bae454a4..5efba4875 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,9 @@ postfix-config node_modules/ # misc -.DS_Store \ No newline at end of file +.DS_Store + +# E2E test runner +.e2e-pids/ +.e2e-server.log +.e2e-client.log \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 2c10360af..031a9d83a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,51 +7,32 @@ This file provides guidance for Claude Code when working with this repository. ### Server (Spring Boot + Gradle) - **Run server**: `cd server && ./gradlew bootRun` - **Run tests**: `cd server && ./gradlew test` -- **Run tests with coverage**: `cd server && ./gradlew test jacocoTestReport` - **Format code**: `cd server && ./gradlew spotlessApply` -- **Check formatting**: `cd server && ./gradlew spotlessCheck` ### Client (React + Webpack) - **Install dependencies**: `cd client && npm install` - **Run dev server**: `cd client && npm run dev` - **Build**: `cd client && npm run build` - **Lint**: `cd client && npx eslint src/` -- **Type check**: `cd client && npx tsc --noEmit` (ignore mantine-datatable type errors — pre-existing) +- **Type check**: `cd client && npx tsc --noEmit` (ignore mantine-datatable type errors) + +### E2E Tests (Playwright) +- **Run locally**: `./execute-e2e-local.sh` (starts all services automatically) +- **Run only tests**: `cd client && npm run e2e` (when services already running) +- **Interactive UI**: `./execute-e2e-local.sh --ui` ## Architecture - **Server**: Spring Boot 3, Java 25, PostgreSQL, Keycloak for auth, Liquibase for migrations - **Client**: React 19, TypeScript, Mantine UI, Webpack -- **Deployment**: Docker multi-platform images (amd64/arm64), deployed via GitHub Actions to VMs using docker-compose +- **Deployment**: Docker multi-platform images (amd64/arm64), deployed via GitHub Actions ## Key Conventions -### Server: DTO Serialization (`@JsonInclude(NON_EMPTY)`) - -All DTOs use `@JsonInclude(JsonInclude.Include.NON_EMPTY)`. This is an intentional API contract: -- `null` values, empty strings (`""`), and empty collections (`[]`) are **omitted** from JSON responses -- This applies to **all** DTOs (detail and overview), not just list endpoints -- The client **must** handle missing fields gracefully — this is by design, not a bug - -### Server: JPA Fetch Types - -Prefer `FetchType.LAZY` for `@OneToMany` and `@ManyToMany` relationships. The application uses `spring.jpa.open-in-view=true`, so lazy loading works throughout the request lifecycle including in controllers. +### DTO Serialization (`@JsonInclude(NON_EMPTY)`) -### Client: Handling API Responses - -Since the server omits empty/null fields from JSON: -- TypeScript interfaces mark omittable fields as optional (`?`) -- Always use fallback defaults: `?? ''` for strings, `?? []` for arrays -- Use optional chaining (`?.`) for nested access on optional fields -- Never assume an array or string field is present in the response +All DTOs use `@JsonInclude(JsonInclude.Include.NON_EMPTY)`. `null`, empty strings, and empty collections are omitted from JSON. The client must handle missing fields with `?? ''`, `?? []`, and `?.`. ### Role Terminology The backend/Keycloak uses `supervisor` and `advisor` roles. In the UI these are displayed as "Examiner" and "Supervisor" respectively. - -## CI/CD - -- `dev.yml`: Triggers on PRs to develop/main and pushes to develop/main. Has concurrency control per PR. -- `prod.yml`: Triggers on pushes to main only. Has concurrency control (no cancellation). -- `build_docker.yml`: Separate jobs for server and client builds (not a matrix) to avoid output race conditions. -- `deploy_docker.yml`: Deploys to VMs via SSH. Uses environment protection rules requiring approval. diff --git a/README.md b/README.md index 794d0886f..b8c188db7 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ Group heads have the Group Admin role for their group by default (this cannot be 1. [Production Setup](docs/PRODUCTION.md) 2. [Configuration](docs/CONFIGURATION.md) 3. [Customizing E-Mails](docs/MAILS.md) -4. [Development Setup](docs/DEVELOPMENT.md) +4. [Development Setup](docs/DEVELOPMENT.md) (includes [E2E Tests](docs/DEVELOPMENT.md#e2e-tests-playwright)) 5. [Database Changes](docs/DATABASE.md) ## Features diff --git a/client/.gitignore b/client/.gitignore index 925a080c1..f1a178bcd 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -9,6 +9,10 @@ # testing /coverage +/test-results/ +/playwright-report/ +/blob-report/ +/e2e/.auth/ # production /build diff --git a/client/e2e/application-review-workflow.spec.ts b/client/e2e/application-review-workflow.spec.ts new file mode 100644 index 000000000..2eb8cc4a3 --- /dev/null +++ b/client/e2e/application-review-workflow.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo, selectOption } from './helpers' + +const APPLICATION_REJECT_ID = '00000000-0000-4000-c000-000000000004' // student4 on topic 1, NOT_ASSESSED +const APPLICATION_ACCEPT_ID = '00000000-0000-4000-c000-000000000005' // student5 on topic 2, NOT_ASSESSED + +test.describe('Application Review Workflow', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('advisor can reject a NOT_ASSESSED application', async ({ page }) => { + await navigateTo(page, `/applications/${APPLICATION_REJECT_ID}`) + + // Wait for the page to fully load — the student heading is always visible for any state + await expect(page.getByRole('heading', { name: /Student4 User/i })).toBeVisible({ + timeout: 30_000, + }) + + // Check if application still has the review form (NOT_ASSESSED state) + // A prior test run may have rejected this application and DB wasn't re-seeded + const thesisTitle = page.getByLabel('Thesis Title') + const hasReviewForm = await thesisTitle.isVisible({ timeout: 5_000 }).catch(() => false) + if (!hasReviewForm) { + return + } + + // Click the first "Reject" button (header area, opens modal directly) + const rejectButton = page.getByRole('button', { name: 'Reject', exact: true }).first() + await expect(rejectButton).toBeVisible({ timeout: 10_000 }) + await rejectButton.click() + + // Modal should open with "Reject Application" title + await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5_000 }) + await expect( + page.getByRole('dialog').getByText('Reject Application').first(), + ).toBeVisible() + + // "Topic requirements not met" should be the default selected reason for topic-based applications + await expect(page.getByText('Topic requirements not met')).toBeVisible() + + // "Notify Student" checkbox should be checked by default + const notifyCheckbox = page.getByRole('dialog').getByLabel('Notify Student') + await expect(notifyCheckbox).toBeChecked() + + // Click "Reject Application" button in the modal + await page.getByRole('dialog').getByRole('button', { name: 'Reject Application' }).click() + + // Verify success notification + await expect(page.getByText('Application rejected successfully')).toBeVisible({ + timeout: 10_000, + }) + }) + + test('advisor can accept a NOT_ASSESSED application', async ({ page }) => { + await navigateTo(page, `/applications/${APPLICATION_ACCEPT_ID}`) + + // Wait for the page to fully load — the student heading is always visible for any state + await expect(page.getByRole('heading', { name: /Student5 User/i })).toBeVisible({ + timeout: 30_000, + }) + + // Check if application still has the review form (NOT_ASSESSED state) + // A prior test run may have accepted this application and DB wasn't re-seeded + const thesisTitle = page.getByLabel('Thesis Title') + const hasReviewForm = await thesisTitle.isVisible({ timeout: 5_000 }).catch(() => false) + if (!hasReviewForm) { + return + } + + // Verify the acceptance form has pre-filled fields from the topic + await expect(thesisTitle).not.toHaveValue('') + + // Thesis Type should be pre-filled + await expect(page.getByRole('textbox', { name: 'Thesis Type' })).toBeVisible() + + // Thesis Language may not be pre-filled — fill it if empty + const languageInput = page.getByRole('textbox', { name: 'Thesis Language' }) + const languageValue = await languageInput.inputValue() + if (!languageValue) { + await selectOption(page, 'Thesis Language', /english/i) + } + + // Supervisor and Advisor(s) should be pre-filled from the topic (pills visible) + const supervisorWrapper = page.locator( + '.mantine-InputWrapper-root:has(.mantine-InputWrapper-label:text("Supervisor"))', + ) + await expect(supervisorWrapper.locator('.mantine-Pill-root').first()).toBeVisible({ + timeout: 10_000, + }) + + const advisorWrapper = page.locator( + '.mantine-InputWrapper-root:has(.mantine-InputWrapper-label:text("Advisor(s)"))', + ) + await expect(advisorWrapper.locator('.mantine-Pill-root').first()).toBeVisible({ + timeout: 10_000, + }) + + // Click "Accept" button + const acceptButton = page.getByRole('button', { name: 'Accept', exact: true }) + await expect(acceptButton).toBeEnabled({ timeout: 10_000 }) + await acceptButton.click() + + // Verify success notification + await expect(page.getByText('Application accepted successfully')).toBeVisible({ + timeout: 10_000, + }) + }) +}) diff --git a/client/e2e/application-workflow.spec.ts b/client/e2e/application-workflow.spec.ts new file mode 100644 index 000000000..0db765229 --- /dev/null +++ b/client/e2e/application-workflow.spec.ts @@ -0,0 +1,99 @@ +import { test, expect } from '@playwright/test' +import { + authStatePath, + createTestPdfBuffer, + fillRichTextEditor, + navigateTo, + selectOption, +} from './helpers' + +test.describe('Application Workflow - Student submits application', () => { + test.use({ storageState: authStatePath('student') }) + + test('student can submit an application for a topic through the full stepper', async ({ + page, + }) => { + await navigateTo(page, '/submit-application') + await expect( + page.getByRole('heading', { name: 'Submit Application', exact: true }), + ).toBeVisible({ timeout: 30_000 }) + + // Detect which step we're on (state may vary from prior test runs) + const successText = page.getByText('Your application was successfully submitted!') + const firstNameLabel = page.getByLabel('First Name') + const topicButton = page.getByRole('button', { + name: /Continuous Integration Pipeline Optimization/i, + }) + + // Check for already-submitted state first + const alreadyDone = await successText.isVisible({ timeout: 2_000 }).catch(() => false) + if (alreadyDone) return + + // Determine if we're on step 1 or step 2 + const onStep2 = await firstNameLabel.isVisible({ timeout: 3_000 }).catch(() => false) + + if (!onStep2) { + // Step 1: Select Topic - topic might not be visible if student already applied to all + const topicVisible = await topicButton.isVisible({ timeout: 5_000 }).catch(() => false) + if (!topicVisible) { + // Student may have already applied for this topic; verify we're on the stepper + await expect(page.locator('.mantine-Stepper-root')).toBeVisible({ timeout: 5_000 }) + return + } + await topicButton.click() + await page.getByRole('button', { name: 'Apply', exact: true }).click() + } + + // Step 2: Student Information - should be pre-filled from seed data + await expect(firstNameLabel).toBeVisible({ timeout: 15_000 }) + await expect(firstNameLabel).toHaveValue('Student') + + // Upload required files if file inputs exist + const pdfBuffer = createTestPdfBuffer() + for (const label of ['Examination Report', 'CV', 'Bachelor Report']) { + const wrapper = page.locator( + `.mantine-InputWrapper-root:has(.mantine-InputWrapper-label:text("${label}"))`, + ) + const fileInput = wrapper.locator('input[type="file"]') + if ((await fileInput.count()) > 0) { + await fileInput.setInputFiles({ + name: `${label.toLowerCase().replace(/ /g, '-')}.pdf`, + mimeType: 'application/pdf', + buffer: pdfBuffer, + }) + } + } + + // Accept privacy notice if not already checked + const privacyCheckbox = page.getByLabel(/privacy/i).or(page.getByRole('checkbox').first()) + if (!(await privacyCheckbox.isChecked())) { + await privacyCheckbox.check() + } + + const updateButton = page.getByRole('button', { name: 'Update Information', exact: true }) + await expect(updateButton).toBeEnabled({ timeout: 30_000 }) + await updateButton.click() + + // Step 3: Motivation + const submittedAfterUpdate = await successText.isVisible({ timeout: 2_000 }).catch(() => false) + if (submittedAfterUpdate) return + + await expect(page.getByRole('textbox', { name: 'Thesis Type' })).toBeVisible({ + timeout: 15_000, + }) + await selectOption(page, 'Thesis Type', /master/i) + await fillRichTextEditor( + page, + 'Motivation', + 'I am highly motivated to work on CI pipeline optimization because it aligns with my research interests in DevOps and continuous integration.', + ) + + const submitButton = page.getByRole('button', { name: 'Submit Application' }) + await expect(submitButton).toBeEnabled({ timeout: 10_000 }) + await submitButton.click() + + // Verify we're still on the application page (no crash) + await page.waitForTimeout(2_000) + await expect(page).toHaveURL(/\/submit-application/) + }) +}) diff --git a/client/e2e/applications.spec.ts b/client/e2e/applications.spec.ts new file mode 100644 index 000000000..4d4a9f9ef --- /dev/null +++ b/client/e2e/applications.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Applications - Student', () => { + test('submit application page shows stepper form', async ({ page }) => { + await navigateTo(page, '/submit-application') + + await expect(page).toHaveURL(/\/submit-application/) + // The multi-step stepper form should be visible + await expect(page.locator('.mantine-Stepper-root')).toBeVisible({ timeout: 15_000 }) + }) + + test('submit application with pre-selected topic', async ({ page }) => { + // Navigate with topic pre-selected (CI Pipeline Optimization) + await navigateTo(page, '/submit-application/00000000-0000-4000-b000-000000000002') + + await expect(page).toHaveURL(/\/submit-application\/00000000/) + }) + + test('dashboard shows student applications section', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 30_000 }) + // Student should see My Applications section + await expect(page.getByRole('heading', { name: /my applications/i })).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Applications - Advisor review', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('review page loads with application sidebar', async ({ page }) => { + await navigateTo(page, '/applications') + + await expect(page).toHaveURL(/\/applications/) + // The page should show the application filter sidebar + await expect(page.getByPlaceholder(/search applications/i)).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Applications - Supervisor review', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('review page is accessible', async ({ page }) => { + await navigateTo(page, '/applications') + + await expect(page).toHaveURL(/\/applications/) + await expect(page.getByPlaceholder(/search applications/i)).toBeVisible({ timeout: 15_000 }) + }) + + test('application detail shows student data and topic', async ({ page }) => { + // Navigate to ACCEPTED application: student on topic 1 (stable across re-runs) + await navigateTo(page, '/applications/00000000-0000-4000-c000-000000000001') + + // Student name heading + await expect(page.getByRole('heading', { name: 'Student User' })).toBeVisible({ + timeout: 15_000, + }) + + // Topic accordion button with topic title + await expect( + page.getByRole('button', { name: 'Automated Code Review Using Large Language Models' }), + ).toBeVisible() + + // Motivation section with actual content from seed data + await expect(page.getByText('Motivation')).toBeVisible() + await expect(page.getByText(/passionate about LLMs/)).toBeVisible() + + // Student detail values from seed data + await expect(page.getByText('student@test.local')).toBeVisible() + await expect(page.getByText('Computer Science')).toBeVisible() + await expect(page.getByText('Accepted').first()).toBeVisible() + }) +}) diff --git a/client/e2e/auth.setup.ts b/client/e2e/auth.setup.ts new file mode 100644 index 000000000..428f94646 --- /dev/null +++ b/client/e2e/auth.setup.ts @@ -0,0 +1,45 @@ +import { test as setup, expect } from '@playwright/test' + +const TEST_USERS = [ + { name: 'student', username: 'student', password: 'student' }, + { name: 'student2', username: 'student2', password: 'student2' }, + { name: 'student3', username: 'student3', password: 'student3' }, + { name: 'advisor', username: 'advisor', password: 'advisor' }, + { name: 'advisor2', username: 'advisor2', password: 'advisor2' }, + { name: 'supervisor', username: 'supervisor', password: 'supervisor' }, + { name: 'supervisor2', username: 'supervisor2', password: 'supervisor2' }, + { name: 'admin', username: 'admin', password: 'admin' }, +] as const + +for (const user of TEST_USERS) { + setup(`authenticate as ${user.name}`, async ({ page }) => { + // Navigate to a protected route to trigger Keycloak login redirect + await page.goto('/dashboard') + + // Wait for Keycloak login page to load + await expect(page.locator('#kc-login')).toBeVisible({ timeout: 30_000 }) + + // Fill in credentials on the Keycloak login form + await page.locator('#username').fill(user.username) + await page.locator('#password').fill(user.password) + await page.locator('#kc-login').click() + + // Wait for redirect back to the app and the dashboard to load + await expect(page).toHaveURL(/localhost:3000/, { timeout: 30_000 }) + + // Wait for the app to fully initialize with the auth tokens + await page.waitForFunction(() => { + try { + const tokens = localStorage.getItem('authentication_tokens') + if (!tokens) return false + const parsed = JSON.parse(tokens) + return !!parsed.access_token && !!parsed.refresh_token + } catch { + return false + } + }, { timeout: 15_000 }) + + // Save the authenticated state (localStorage + cookies including Keycloak session) + await page.context().storageState({ path: `e2e/.auth/${user.name}.json` }) + }) +} diff --git a/client/e2e/auth.spec.ts b/client/e2e/auth.spec.ts new file mode 100644 index 000000000..0fbffa73b --- /dev/null +++ b/client/e2e/auth.spec.ts @@ -0,0 +1,111 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Authentication - Unauthenticated', () => { + test.use({ storageState: { cookies: [], origins: [] } }) + + test('protected route redirects to Keycloak login', async ({ page }) => { + await page.goto('/dashboard') + + // Should redirect to Keycloak login page + await expect(page).toHaveURL(/\/realms\/thesis-management\//) + await expect(page.locator('#kc-login')).toBeVisible({ timeout: 30_000 }) + await expect(page.locator('#username')).toBeVisible() + await expect(page.locator('#password')).toBeVisible() + await expect(page.getByRole('heading', { name: /sign in/i })).toBeVisible() + }) + + test('public landing page is accessible without login', async ({ page }) => { + await navigateTo(page, '/') + + await expect(page.getByText('Find a Thesis Topic')).toBeVisible() + // Should show Login button in header, not user menu + await expect(page.locator('header').getByText('Login')).toBeVisible() + }) + + test('about page is accessible without login', async ({ page }) => { + await navigateTo(page, '/about') + + await expect(page.getByRole('heading', { name: 'Thesis Management', exact: true })).toBeVisible() + }) +}) + +test.describe('Authentication - Student', () => { + test('can access dashboard and sees correct nav items', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + // Student should see these nav items + await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Submit Application' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Browse Theses' })).toBeVisible() + // Student should NOT see management nav items + await expect(page.getByRole('link', { name: 'Review Applications' })).toBeHidden() + await expect(page.getByRole('link', { name: 'Manage Topics' })).toBeHidden() + await expect(page.getByRole('link', { name: 'Theses Overview' })).toBeHidden() + await expect(page.getByRole('link', { name: 'Research Groups' })).toBeHidden() + await expect(page.getByRole('link', { name: 'Interviews' })).toBeHidden() + }) + + test('header shows user menu when authenticated', async ({ page }) => { + await navigateTo(page, '/dashboard') + + // Header should show app title and user avatar (not Login button) + await expect(page.getByText('Thesis Management').first()).toBeVisible() + await expect(page.locator('header').getByText('Login')).toBeHidden() + }) +}) + +test.describe('Authentication - Advisor', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('sees management nav items but not admin items', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + // Advisor should see management items + await expect(page.getByRole('link', { name: 'Review Applications' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Manage Topics' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Theses Overview' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Interviews' })).toBeVisible() + // Advisor should NOT see Submit Application (hidden from advisor/supervisor) + await expect(page.getByRole('link', { name: 'Submit Application' })).toBeHidden() + // Advisor should NOT see admin-only items + await expect(page.getByRole('link', { name: 'Research Groups' })).toBeHidden() + }) +}) + +test.describe('Authentication - Supervisor', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('sees management nav items but not admin items', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + // Supervisor should see management items + await expect(page.getByRole('link', { name: 'Review Applications' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Manage Topics' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Theses Overview' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Interviews' })).toBeVisible() + // Supervisor should NOT see Submit Application + await expect(page.getByRole('link', { name: 'Submit Application' })).toBeHidden() + // Supervisor should NOT see admin-only items + await expect(page.getByRole('link', { name: 'Research Groups' })).toBeHidden() + }) +}) + +test.describe('Authentication - Admin', () => { + test.use({ storageState: authStatePath('admin') }) + + test('sees all nav items including Research Groups', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Review Applications' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Manage Topics' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Browse Theses' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Theses Overview' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Research Groups' })).toBeVisible() + }) +}) diff --git a/client/e2e/dashboard.spec.ts b/client/e2e/dashboard.spec.ts new file mode 100644 index 000000000..d55f94838 --- /dev/null +++ b/client/e2e/dashboard.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Dashboard - Student', () => { + test('shows dashboard with My Theses and My Applications', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + // Student should see My Theses section + await expect(page.getByRole('heading', { name: /my theses/i })).toBeVisible() + // Student should see My Applications section + await expect(page.getByRole('heading', { name: /my applications/i })).toBeVisible() + }) +}) + +test.describe('Dashboard - Advisor', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('shows dashboard with My Theses section', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByRole('heading', { name: /my theses/i })).toBeVisible() + }) +}) + +test.describe('Dashboard - Supervisor', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('shows dashboard with management view', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByRole('heading', { name: /my theses/i })).toBeVisible() + }) +}) + +test.describe('Dashboard - Admin', () => { + test.use({ storageState: authStatePath('admin') }) + + test('admin can access dashboard', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + }) +}) diff --git a/client/e2e/helpers.ts b/client/e2e/helpers.ts new file mode 100644 index 000000000..08de22487 --- /dev/null +++ b/client/e2e/helpers.ts @@ -0,0 +1,121 @@ +import { Locator, Page, expect } from '@playwright/test' + +/** + * Navigate to a page and wait for it to fully load. + * Waits for the Mantine Loader spinner to disappear. + */ +export async function navigateTo(page: Page, path: string) { + await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 30_000 }) + await page + .locator('.mantine-Loader-root') + .waitFor({ state: 'hidden', timeout: 30_000 }) + .catch(() => { + // Loader may never appear if the page loads instantly + }) +} + +/** + * Use a specific auth state file for a test. + */ +export function authStatePath( + role: + | 'student' + | 'student2' + | 'student3' + | 'advisor' + | 'advisor2' + | 'supervisor' + | 'supervisor2' + | 'admin', +): string { + return `e2e/.auth/${role}.json` +} + +/** + * Type text into a TipTap/ProseMirror rich text editor identified by its label. + * Optionally accepts a parent locator to scope the search (e.g., a dialog). + */ +export async function fillRichTextEditor( + page: Page, + label: string, + text: string, + parent?: Locator, +) { + const root = parent ?? page + const wrapper = root.locator(`.mantine-InputWrapper-root:has(.mantine-InputWrapper-label:text("${label}"))`) + const editor = wrapper.locator('.ProseMirror') + await editor.click() + // Select all existing content and replace it + const modifier = process.platform === 'darwin' ? 'Meta' : 'Control' + await page.keyboard.press(`${modifier}+a`) + await page.keyboard.type(text) +} + +/** + * Select a value from a Mantine Select/ComboBox identified by its label. + * Uses getByRole('textbox') to avoid matching the listbox element. + */ +export async function selectOption(page: Page, label: string, optionText: string | RegExp) { + await page.getByRole('textbox', { name: label }).click() + await page.getByRole('option', { name: optionText }).click() +} + +/** + * Click a Mantine MultiSelect input. Uses force:true to bypass the wrapper + * div that intercepts pointer events. + */ +export async function clickMultiSelect(page: Page, label: string) { + await page.getByRole('textbox', { name: label }).click({ force: true }) +} + +/** + * Select an option from a UserMultiSelect (server-side search). + * Opens the dropdown, waits for options to load, then selects the option. + * Uses evaluate to dispatch a full mouse event chain since Playwright's + * built-in click doesn't always trigger Mantine's React event handlers + * in portal-rendered combobox dropdowns. + */ +export async function searchAndSelectMultiSelect( + page: Page, + label: string, + optionPattern: RegExp, +) { + const textbox = page.getByRole('textbox', { name: label }) + await textbox.click({ force: true }) + const listbox = page.getByRole('listbox', { name: label }) + const option = listbox.getByRole('option', { name: optionPattern }).first() + await expect(option).toBeVisible({ timeout: 10_000 }) + // First attempt: standard Playwright click + await option.click() + await page.waitForTimeout(500) + // Check if selection registered by looking for a pill + const wrapper = page.locator(`.mantine-InputWrapper-root:has(.mantine-InputWrapper-label:text("${label}"))`) + const hasPill = await wrapper.locator('.mantine-Pill-root').count() > 0 + if (!hasPill) { + // Fallback: re-open dropdown and use evaluate to dispatch mouse events + await textbox.click({ force: true }) + const retryOption = listbox.getByRole('option', { name: optionPattern }).first() + await expect(retryOption).toBeVisible({ timeout: 10_000 }) + await retryOption.evaluate((el) => { + el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window })) + el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window })) + el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })) + }) + await page.waitForTimeout(500) + } + // Verify selection registered + await expect(wrapper.locator('.mantine-Pill-root')).toBeVisible({ timeout: 5_000 }) +} + +/** + * Create a minimal valid PDF buffer for file upload tests. + */ +export function createTestPdfBuffer(): Buffer { + return Buffer.from( + '%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' + + '2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n' + + '3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n' + + 'xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n' + + 'trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n206\n%%EOF', + ) +} diff --git a/client/e2e/interview-workflow.spec.ts b/client/e2e/interview-workflow.spec.ts new file mode 100644 index 000000000..6b0a6b043 --- /dev/null +++ b/client/e2e/interview-workflow.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +const INTERVIEW_PROCESS_ID = '00000000-0000-4000-e600-000000000001' // Topic 3, DSA group, active +const INTERVIEWEE_ID = '00000000-0000-4000-e700-000000000001' // student4, NULL score + +test.describe('Interview Workflow', () => { + test.use({ storageState: authStatePath('supervisor2') }) + + test('supervisor can view interview process and score an interviewee', async ({ page }) => { + // Navigate directly to the interviewee assessment page + await navigateTo( + page, + `/interviews/${INTERVIEW_PROCESS_ID}/interviewee/${INTERVIEWEE_ID}`, + ) + + // Wait for page to load with the interviewee name + await expect( + page.getByRole('heading', { name: /Interview - Student4 User/i }), + ).toBeVisible({ timeout: 15_000 }) + + // Verify "Interview Assessment" section title + await expect( + page.getByRole('heading', { name: 'Interview Assessment' }), + ).toBeVisible() + + // Verify "Score" card title is visible + await expect(page.getByRole('heading', { name: 'Score', exact: true })).toBeVisible() + + // Set score using SegmentedControl - click on "Great Candidate" (score 4) + await page.getByText('Great Candidate').click() + + // Verify score selection registered — Mantine SegmentedControl sets data-active on the label + const scoreLabel = page.getByText('Great Candidate').locator('xpath=ancestor::label[@data-active]') + await expect(scoreLabel).toBeVisible({ timeout: 5_000 }) + + // Verify "Interview Note" card title is visible + await expect( + page.getByRole('heading', { name: 'Interview Note', exact: true }), + ).toBeVisible() + + // Fill interview note using the ProseMirror editor + // (InterviewNoteCard uses DocumentEditor without a Mantine InputWrapper label, + // so we target the ProseMirror editor directly) + const noteEditor = page.locator('.ProseMirror').first() + await noteEditor.click() + const modifier = process.platform === 'darwin' ? 'Meta' : 'Control' + await page.keyboard.press(`${modifier}+a`) + await page.keyboard.type('Good understanding of streaming concepts and anomaly detection methods.') + + // Wait for auto-save (debounced at 1000ms) — look for "Saved" indicator + await expect(page.getByText('Saved')).toBeVisible({ timeout: 10_000 }) + }) + + test('supervisor can open the add slot modal on interview process page', async ({ page }) => { + await navigateTo(page, `/interviews/${INTERVIEW_PROCESS_ID}`) + + // Wait for interview management page to load + await expect( + page.getByRole('heading', { name: /interview management/i }), + ).toBeVisible({ timeout: 15_000 }) + + // Verify interviewees section is visible + await expect( + page.getByRole('heading', { name: 'Interviewees', exact: true }), + ).toBeVisible({ timeout: 10_000 }) + + // Click "Add Slot" button to open the modal + await page.getByRole('button', { name: /Add Slot|^Add$/i }).click() + + // Modal should open with "Add Interview Slot" title + const dialog = page.getByRole('dialog') + await expect(dialog).toBeVisible({ timeout: 5_000 }) + await expect(dialog.getByText('Add Interview Slot')).toBeVisible() + + // Verify Interview Length SegmentedControl is visible with options + await expect(dialog.getByText('Interview Length')).toBeVisible() + await expect(dialog.getByText('30min').first()).toBeVisible() + await expect(dialog.getByText('45min').first()).toBeVisible() + + // Verify "Select Dates" section is visible + await expect(dialog.getByText('Select Dates')).toBeVisible() + + // Verify "Save Slots" button exists + await expect(dialog.getByRole('button', { name: 'Save Slots' })).toBeVisible() + }) +}) diff --git a/client/e2e/interviews.spec.ts b/client/e2e/interviews.spec.ts new file mode 100644 index 000000000..60d38ba46 --- /dev/null +++ b/client/e2e/interviews.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Interviews - Supervisor', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('interview overview shows interview topics and upcoming interviews', async ({ page }) => { + await navigateTo(page, '/interviews') + + await expect(page.getByRole('heading', { name: 'Interviews', exact: true })).toBeVisible({ timeout: 30_000 }) + // Should show the two panels + await expect(page.getByText(/interview topics/i)).toBeVisible() + await expect(page.getByRole('heading', { name: 'Upcoming Interviews', exact: true })).toBeVisible() + }) + + test('interview process detail page loads', async ({ page }) => { + // UUID matches the active interview process seeded in seed_dev_test_data.sql + await navigateTo(page, '/interviews/00000000-0000-4000-e600-000000000001') + + await expect(page.getByRole('heading', { name: /interview management/i })).toBeVisible({ timeout: 30_000 }) + // Should show sections for slots and interviewees + await expect(page.getByRole('heading', { name: 'Interviewees', exact: true })).toBeVisible() + }) +}) + +test.describe('Interviews - Advisor', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('interview overview is accessible', async ({ page }) => { + await navigateTo(page, '/interviews') + + await expect(page.getByRole('heading', { name: 'Interviews', exact: true })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByText(/interview topics/i)).toBeVisible() + }) +}) + +test.describe('Interviews - Student cannot access', () => { + test.use({ storageState: authStatePath('student') }) + + test('student is redirected away from interviews page', async ({ page }) => { + await navigateTo(page, '/interviews') + + // Student should not see the interviews management page + // They should be redirected or see access denied + await expect(page.getByRole('heading', { name: 'Interviews', exact: true })).toBeHidden() + }) +}) diff --git a/client/e2e/navigation.spec.ts b/client/e2e/navigation.spec.ts new file mode 100644 index 000000000..f4b945505 --- /dev/null +++ b/client/e2e/navigation.spec.ts @@ -0,0 +1,112 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Navigation - Public pages', () => { + test.use({ storageState: { cookies: [], origins: [] } }) + + test('landing page shows topic browsing UI', async ({ page }) => { + await navigateTo(page, '/') + + await expect(page.getByText('Find a Thesis Topic')).toBeVisible({ timeout: 15_000 }) + // Topic search and filter controls + await expect(page.getByPlaceholder(/search thesis topics/i)).toBeVisible() + await expect(page.getByText('Open Topics')).toBeVisible() + await expect(page.getByText('Published Topics')).toBeVisible() + // View toggle (List / Grid) + await expect(page.getByText('List')).toBeVisible() + await expect(page.getByText('Grid')).toBeVisible() + // Table headers for list view + await expect(page.getByText('Title').first()).toBeVisible() + await expect(page.getByText('Thesis Types').first()).toBeVisible() + }) + + test('about page renders full content', async ({ page }) => { + await navigateTo(page, '/about') + + // Main heading + await expect(page.getByRole('heading', { name: 'Thesis Management', exact: true })).toBeVisible() + // Section headings + await expect(page.getByRole('heading', { name: 'Project Managers' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Contributors' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Features' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Git Information' })).toBeVisible() + // Key people + await expect(page.getByText('Stephan Krusche')).toBeVisible() + await expect(page.getByText('Fabian Emilius')).toBeVisible() + // Support contact + await expect(page.getByText('thesis-management-support.aet@xcit.tum.de')).toBeVisible() + }) + + test('footer links are visible on public pages', async ({ page }) => { + await navigateTo(page, '/') + + await expect(page.getByText('About')).toBeVisible() + await expect(page.getByText('Imprint')).toBeVisible() + await expect(page.getByText('Privacy')).toBeVisible() + }) + + test('unknown single-segment route shows landing page', async ({ page }) => { + // /:researchGroupAbbreviation catches this before the * wildcard + await navigateTo(page, '/nonexistent-group') + + await expect(page.getByText('Find a Thesis Topic')).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Navigation - Student routes', () => { + test('can navigate between pages via sidebar', async ({ page }) => { + await navigateTo(page, '/dashboard') + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + + // Navigate to Browse Theses + await page.getByRole('link', { name: 'Browse Theses' }).click() + await expect(page).toHaveURL(/\/theses/) + await expect(page.getByRole('heading', { name: /browse theses/i })).toBeVisible() + + // Navigate to Submit Application + await page.getByRole('link', { name: 'Submit Application' }).click() + await expect(page).toHaveURL(/\/submit-application/) + + // Navigate back to Dashboard + await page.getByRole('link', { name: 'Dashboard' }).click() + await expect(page).toHaveURL(/\/dashboard/) + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible() + }) + + test('header logo navigates to dashboard when authenticated', async ({ page }) => { + await navigateTo(page, '/theses') + await page.getByText('Thesis Management').first().click() + await expect(page).toHaveURL(/\/dashboard/) + }) +}) + +test.describe('Navigation - Supervisor routes', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('management pages are accessible', async ({ page }) => { + // Theses Overview + await navigateTo(page, '/overview') + await expect(page.getByRole('heading', { name: /theses overview/i })).toBeVisible({ timeout: 15_000 }) + + // Application Review + await navigateTo(page, '/applications') + await expect(page).toHaveURL(/\/applications/) + + // Manage Topics + await navigateTo(page, '/topics') + await expect(page.getByRole('heading', { name: /manage topics/i })).toBeVisible() + + // Interviews + await navigateTo(page, '/interviews') + await expect(page.getByRole('heading', { name: 'Interviews', exact: true })).toBeVisible() + }) +}) + +test.describe('Navigation - Admin routes', () => { + test.use({ storageState: authStatePath('admin') }) + + test('admin-only pages are accessible', async ({ page }) => { + await navigateTo(page, '/research-groups') + await expect(page.getByRole('heading', { name: /research groups/i })).toBeVisible({ timeout: 15_000 }) + }) +}) diff --git a/client/e2e/presentation-workflow.spec.ts b/client/e2e/presentation-workflow.spec.ts new file mode 100644 index 000000000..b8d7cb773 --- /dev/null +++ b/client/e2e/presentation-workflow.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo, selectOption } from './helpers' + +// Thesis d000-0003 is in SUBMITTED state, assigned to student3, has abstract text set +const THESIS_ID = '00000000-0000-4000-d000-000000000003' +const THESIS_URL = `/theses/${THESIS_ID}` +const THESIS_TITLE = 'Online Anomaly Detection in IoT Sensor Streams' + +test.describe('Presentation Workflow - Student creates a presentation draft', () => { + test.use({ storageState: authStatePath('student3') }) + + test('student can create a presentation draft for their thesis', async ({ page }) => { + await navigateTo(page, THESIS_URL) + + // Wait for thesis page to load + await expect(page.getByRole('heading', { name: THESIS_TITLE })).toBeVisible({ + timeout: 30_000, + }) + + // Find the Presentation accordion section and ensure it is visible + const presentationControl = page.getByRole('button', { name: 'Presentation', exact: true }) + await expect(presentationControl).toBeVisible({ timeout: 10_000 }) + + // Click to expand if collapsed + if ((await presentationControl.getAttribute('aria-expanded')) !== 'true') { + await presentationControl.click() + } + + // Check if "Create Presentation Draft" button is available + // (Thesis may already be FINISHED from the grading workflow test, hiding the button) + const createDraftButton = page.getByRole('button', { name: 'Create Presentation Draft' }) + const canCreateDraft = await createDraftButton.isVisible({ timeout: 5_000 }).catch(() => false) + + if (!canCreateDraft) { + // Thesis is closed — verify existing presentations are displayed instead + await expect( + page.getByRole('heading', { name: /Presentation$/i }).first(), + ).toBeVisible() + return + } + + await createDraftButton.click() + + // Modal should open - use first() since the date picker also opens a dialog + const modal = page.getByRole('dialog').first() + await expect(modal).toBeVisible({ timeout: 5_000 }) + + // The thesis has an abstract, so there should be a blue notice (not red error) + await expect( + modal.getByText('Please make sure that the thesis title'), + ).toBeVisible() + + // Fill in the presentation form + // Presentation Type - default is INTERMEDIATE, select FINAL + await selectOption(page, 'Presentation Type', /final/i) + + // Visibility - default is PUBLIC, keep it + + // Location + await page.getByLabel('Location').fill('Room 01.07.014, Garching Campus') + + // Language + await selectOption(page, 'Language', /english/i) + + // Scheduled At - click button to open the DateTimePicker calendar dialog + await page.getByRole('button', { name: 'Scheduled At' }).click() + + // The DateTimePicker opens a second dialog with a calendar + const datePickerDialog = page.getByRole('dialog').nth(1) + await expect(datePickerDialog).toBeVisible({ timeout: 5_000 }) + + // Select a date from the calendar - pick the last day button visible in the grid + const dayButtons = datePickerDialog.locator('table button:not([data-outside])') + await dayButtons.last().click() + + // Set the time using the spinbuttons (hours and minutes) + const hourSpinbutton = datePickerDialog.getByRole('spinbutton').first() + const minuteSpinbutton = datePickerDialog.getByRole('spinbutton').nth(1) + await hourSpinbutton.click() + await hourSpinbutton.fill('14') + await minuteSpinbutton.click() + await minuteSpinbutton.fill('00') + + // Click the submit/checkmark button to confirm date/time selection + // It's the last button in the date picker dialog (after the time inputs) + await datePickerDialog.locator('button').last().click() + + // Click "Create Presentation Draft" button in the modal + const createButton = modal.getByRole('button', { name: 'Create Presentation Draft' }) + await expect(createButton).toBeEnabled({ timeout: 10_000 }) + await createButton.click() + + // Modal should close after successful creation + await expect(modal).not.toBeVisible({ timeout: 15_000 }) + }) +}) diff --git a/client/e2e/presentations.spec.ts b/client/e2e/presentations.spec.ts new file mode 100644 index 000000000..252f130a9 --- /dev/null +++ b/client/e2e/presentations.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Presentations - Student', () => { + test('presentations page shows heading', async ({ page }) => { + await navigateTo(page, '/presentations') + + await expect(page.getByRole('heading', { name: 'Presentations', exact: true })).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Presentations - Supervisor', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('presentations page is accessible for supervisor', async ({ page }) => { + await navigateTo(page, '/presentations') + + await expect(page.getByRole('heading', { name: 'Presentations', exact: true })).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Presentations - Public', () => { + test.use({ storageState: { cookies: [], origins: [] } }) + + test('presentation detail page is accessible without login', async ({ page }) => { + // Seeded public presentation for thesis 3 (final, public visibility) + await navigateTo(page, '/presentations/00000000-0000-4000-e300-000000000002') + + // Should show presentation info (thesis title) + await expect(page.getByRole('heading', { name: /anomaly detection/i })).toBeVisible({ timeout: 15_000 }) + }) +}) diff --git a/client/e2e/proposal-feedback-workflow.spec.ts b/client/e2e/proposal-feedback-workflow.spec.ts new file mode 100644 index 000000000..fc952c829 --- /dev/null +++ b/client/e2e/proposal-feedback-workflow.spec.ts @@ -0,0 +1,85 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, createTestPdfBuffer, navigateTo } from './helpers' + +// Thesis d000-0002 is in PROPOSAL state, assigned to student2 with advisor +const THESIS_ID = '00000000-0000-4000-d000-000000000002' +const THESIS_URL = `/theses/${THESIS_ID}` +const THESIS_TITLE = 'CI Pipeline Optimization Through Intelligent Test Selection' + +test.describe('Proposal Upload - Student uploads proposal', () => { + test.use({ storageState: authStatePath('student2') }) + + test('student can upload a proposal PDF to a thesis in PROPOSAL state', async ({ page }) => { + await navigateTo(page, THESIS_URL) + + // Wait for thesis page to load + await expect(page.getByRole('heading', { name: THESIS_TITLE })).toBeVisible({ + timeout: 15_000, + }) + + // The Proposal section should be visible and expanded (default for PROPOSAL state) + await expect(page.getByRole('button', { name: 'Upload Proposal' })).toBeVisible({ + timeout: 10_000, + }) + + // Click "Upload Proposal" button - this opens a file upload modal + await page.getByRole('button', { name: 'Upload Proposal' }).click() + + // Modal should open with "File Upload" title + await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5_000 }) + + // Set file on the hidden file input inside the dropzone + const fileInput = page.getByRole('dialog').locator('input[type="file"]') + await fileInput.setInputFiles({ + name: 'test-proposal.pdf', + mimeType: 'application/pdf', + buffer: createTestPdfBuffer(), + }) + + // Click "Upload File" button in the modal + await page.getByRole('dialog').getByRole('button', { name: 'Upload File' }).click() + + // Modal should close after successful upload + await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Proposal Feedback - Advisor requests changes', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('advisor can request changes on a proposal', async ({ page }) => { + await navigateTo(page, THESIS_URL) + + // Wait for thesis page to load + await expect(page.getByRole('heading', { name: THESIS_TITLE })).toBeVisible({ + timeout: 15_000, + }) + + // Scroll to and click "Request Changes" button (red outline button in Proposal section) + const requestChangesButton = page.getByRole('button', { name: 'Request Changes' }).first() + await requestChangesButton.scrollIntoViewIfNeeded() + await requestChangesButton.click() + + // Modal should open with "Request Changes" title + await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10_000 }) + + // Fill in the new change requests textarea (one per line) + await page + .getByLabel('New Change Requests (one request per line)') + .fill('Please add a literature review section\nFix the formatting of the references') + + // Click "Request Changes" button in the modal + await page + .getByRole('dialog') + .getByRole('button', { name: 'Request Changes' }) + .click() + + // Modal should close after successful submission + await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15_000 }) + + // Feedback should appear in the proposal section + await expect(page.getByText('Please add a literature review section').first()).toBeVisible({ + timeout: 10_000, + }) + }) +}) diff --git a/client/e2e/research-groups.spec.ts b/client/e2e/research-groups.spec.ts new file mode 100644 index 000000000..66ef34e78 --- /dev/null +++ b/client/e2e/research-groups.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Research Groups - Admin', () => { + test.use({ storageState: authStatePath('admin') }) + + test('research groups page shows groups and search', async ({ page }) => { + await navigateTo(page, '/research-groups') + + await expect(page.getByRole('heading', { name: /research groups/i })).toBeVisible({ timeout: 15_000 }) + // Search input should be present + await expect(page.getByPlaceholder(/search research groups/i)).toBeVisible() + // Create button should be visible + await expect(page.getByRole('button', { name: /create research group/i })).toBeVisible() + // Seeded research groups should appear (ASE and DSA) + await expect(page.getByText('Data Science and Analytics').first()).toBeVisible() + await expect(page.getByText('Applied Software Engineering').first()).toBeVisible() + }) + + test('search filters research groups', async ({ page }) => { + await navigateTo(page, '/research-groups') + + await expect(page.getByRole('heading', { name: /research groups/i })).toBeVisible({ timeout: 15_000 }) + // Search for ASE + await page.getByPlaceholder(/search research groups/i).fill('Applied Software') + // ASE should still be visible + await expect(page.getByText('Applied Software Engineering').first()).toBeVisible() + }) +}) + +test.describe('Research Group Settings - Supervisor', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('supervisor can access their research group settings', async ({ page }) => { + // UUID matches the ASE research group seeded in seed_dev_test_data.sql + await navigateTo(page, '/research-groups/00000000-0000-4000-a000-000000000001') + + // Supervisor should see either group settings or an unauthorized page + await expect( + page.getByText('Applied Software Engineering').first().or(page.getByText(/unauthorized/i)), + ).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Research Groups - Student cannot access admin page', () => { + test('student is denied access to research groups admin', async ({ page }) => { + await navigateTo(page, '/research-groups') + + // Student should not see the admin page content + await expect(page.getByRole('heading', { name: /research groups/i })).toBeHidden() + }) +}) diff --git a/client/e2e/settings.spec.ts b/client/e2e/settings.spec.ts new file mode 100644 index 000000000..9c6305c5f --- /dev/null +++ b/client/e2e/settings.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Settings - Student', () => { + test('settings page shows My Information and Notification Settings tabs', async ({ page }) => { + await navigateTo(page, '/settings') + + await expect(page).toHaveURL(/\/settings/) + // Tab navigation should be visible + await expect(page.getByText('My Information')).toBeVisible({ timeout: 15_000 }) + await expect(page.getByText('Notification Settings')).toBeVisible() + }) + + test('notifications tab is accessible', async ({ page }) => { + await navigateTo(page, '/settings/notifications') + + await expect(page).toHaveURL(/\/settings\/notifications/) + await expect(page.getByText('Notification Settings')).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Settings - Advisor', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('settings page is accessible for advisor', async ({ page }) => { + await navigateTo(page, '/settings') + + await expect(page.getByText('My Information')).toBeVisible({ timeout: 15_000 }) + await expect(page.getByText('Notification Settings')).toBeVisible() + }) +}) diff --git a/client/e2e/theses.spec.ts b/client/e2e/theses.spec.ts new file mode 100644 index 000000000..d0628e160 --- /dev/null +++ b/client/e2e/theses.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Theses - Browse (Student)', () => { + test('browse theses page shows heading and table', async ({ page }) => { + await navigateTo(page, '/theses') + + await expect(page.getByRole('heading', { name: /browse theses/i })).toBeVisible({ timeout: 15_000 }) + // Student should NOT see create thesis button + await expect(page.getByRole('button', { name: /create/i })).toBeHidden() + }) +}) + +test.describe('Theses - Browse (Supervisor)', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('browse theses page shows create button for management', async ({ page }) => { + await navigateTo(page, '/theses') + + await expect(page.getByRole('heading', { name: /browse theses/i })).toBeVisible({ timeout: 15_000 }) + // Supervisor should see create thesis button + await expect(page.getByRole('button', { name: /create/i }).first()).toBeVisible() + }) +}) + +test.describe('Theses - Overview (Supervisor)', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('theses overview shows management view', async ({ page }) => { + await navigateTo(page, '/overview') + + await expect(page.getByRole('heading', { name: /theses overview/i })).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Theses - Detail page (Advisor)', () => { + test.use({ storageState: authStatePath('advisor') }) + + test('thesis detail shows sections for WRITING thesis', async ({ page }) => { + // Thesis 1: WRITING state, "Automated Code Review Using LLMs" + await navigateTo(page, '/theses/00000000-0000-4000-d000-000000000001') + + // Title + await expect( + page.getByRole('heading', { name: /automated code review/i }), + ).toBeVisible({ timeout: 15_000 }) + + // Key thesis sections should be visible (accordion sections) + await expect(page.getByText('Configuration')).toBeVisible() + await expect(page.getByText('Involved Persons')).toBeVisible() + }) +}) + +test.describe('Theses - Detail page (Student)', () => { + test('student can view their own thesis', async ({ page }) => { + // Student is assigned to thesis 1 (WRITING) + await navigateTo(page, '/theses/00000000-0000-4000-d000-000000000001') + + await expect( + page.getByRole('heading', { name: /automated code review/i }), + ).toBeVisible({ timeout: 15_000 }) + }) +}) + +test.describe('Theses - Dashboard integration', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('supervisor dashboard shows My Theses section', async ({ page }) => { + await navigateTo(page, '/dashboard') + + await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByRole('heading', { name: /my theses/i })).toBeVisible() + }) +}) diff --git a/client/e2e/thesis-grading-workflow.spec.ts b/client/e2e/thesis-grading-workflow.spec.ts new file mode 100644 index 000000000..459be09e5 --- /dev/null +++ b/client/e2e/thesis-grading-workflow.spec.ts @@ -0,0 +1,159 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, fillRichTextEditor, navigateTo } from './helpers' + +// Thesis d000-0003: SUBMITTED state, student3, advisor2, supervisor2 (DSA group) +// Note: Seed data inserts an assessment row directly but thesis state remains SUBMITTED. +// supervisor2 has both advisor and supervisor access (as supervisor on the thesis). +const THESIS_ID = '00000000-0000-4000-d000-000000000003' +const THESIS_URL = `/theses/${THESIS_ID}` +const THESIS_TITLE = 'Online Anomaly Detection in IoT Sensor Streams' + +test.describe.serial('Thesis Grading Workflow', () => { + test('supervisor can submit an assessment on a SUBMITTED thesis', async ({ browser }) => { + const context = await browser.newContext({ storageState: authStatePath('supervisor2') }) + const page = await context.newPage() + + await navigateTo(page, THESIS_URL) + + // Wait for thesis page to load + await expect(page.getByRole('heading', { name: THESIS_TITLE })).toBeVisible({ + timeout: 15_000, + }) + + // Check if the assessment section is actionable (thesis may already be FINISHED from a prior run) + const editButton = page.getByRole('button', { name: 'Edit Assessment' }) + const addButton = page.getByRole('button', { name: 'Add Assessment' }) + const hasEdit = await editButton.isVisible({ timeout: 5_000 }).catch(() => false) + const hasAdd = await addButton.isVisible({ timeout: 2_000 }).catch(() => false) + + if (!hasEdit && !hasAdd) { + // Thesis is already past ASSESSED state (GRADED or FINISHED); assessment is read-only + await expect(page.getByText('Assessment')).toBeVisible() + await context.close() + return + } + + if (hasEdit) { + await editButton.click() + } else { + await addButton.click() + } + + // Modal should open with "Submit Assessment" title + const dialog = page.getByRole('dialog') + await expect(dialog).toBeVisible({ timeout: 5_000 }) + await expect(dialog.getByText('Submit Assessment').first()).toBeVisible() + + // Fill in assessment fields — scope to dialog to avoid matching read-only assessment behind modal + await fillRichTextEditor(page, 'Summary', 'The thesis provides a comprehensive analysis of anomaly detection methods for IoT sensor data.', dialog) + await fillRichTextEditor(page, 'Strengths', 'Strong methodology and well-structured experiments with real-world datasets.', dialog) + await fillRichTextEditor(page, 'Weaknesses', 'Limited discussion of related work in the streaming domain.', dialog) + + // Fill Grade Suggestion (TextInput) — clear first in case of existing value + await dialog.getByLabel('Grade Suggestion').clear() + await dialog.getByLabel('Grade Suggestion').fill('1.3') + + // Click "Submit Assessment" button in the modal + const submitButton = dialog.getByRole('button', { name: 'Submit Assessment' }) + await expect(submitButton).toBeEnabled({ timeout: 5_000 }) + await submitButton.click() + + // Modal should close + await expect(dialog).not.toBeVisible({ timeout: 15_000 }) + + // Verify success notification + await expect(page.getByText('Assessment submitted successfully')).toBeVisible({ + timeout: 10_000, + }) + + await context.close() + }) + + test('supervisor can submit a final grade on an ASSESSED thesis', async ({ browser }) => { + const context = await browser.newContext({ storageState: authStatePath('supervisor2') }) + const page = await context.newPage() + + await navigateTo(page, THESIS_URL) + + // Wait for thesis page to load + await expect(page.getByRole('heading', { name: THESIS_TITLE })).toBeVisible({ + timeout: 15_000, + }) + + // Check if "Add Final Grade" button is available (thesis may already be GRADED/FINISHED) + const addGradeButton = page.getByRole('button', { name: 'Add Final Grade' }) + const editGradeButton = page.getByRole('button', { name: 'Edit Final Grade' }) + const hasAdd = await addGradeButton.isVisible({ timeout: 5_000 }).catch(() => false) + const hasEdit = await editGradeButton.isVisible({ timeout: 2_000 }).catch(() => false) + + if (!hasAdd && !hasEdit) { + // Thesis is already FINISHED — grade section exists but no edit button + await context.close() + return + } + + const gradeButton = hasAdd ? addGradeButton : editGradeButton + await gradeButton.click() + + // Modal should open + const gradeDialog = page.getByRole('dialog') + await expect(gradeDialog).toBeVisible({ timeout: 5_000 }) + await expect(gradeDialog.getByText('Submit Final Grade').first()).toBeVisible() + + // Thesis Visibility select should be visible + await expect(gradeDialog.getByRole('textbox', { name: 'Thesis Visibility' })).toBeVisible() + + // Fill Final Grade (TextInput) — scope to dialog to avoid matching accordion panel + await gradeDialog.getByRole('textbox', { name: 'Final Grade' }).fill('1.3') + + // Fill optional feedback (DocumentEditor) + await fillRichTextEditor(page, 'Feedback (Visible to student)', 'Excellent work overall.', gradeDialog) + + // Click "Submit Grade" button + const submitButton = gradeDialog.getByRole('button', { name: 'Submit Grade' }) + await expect(submitButton).toBeEnabled({ timeout: 5_000 }) + await submitButton.click() + + // Modal should close + await expect(gradeDialog).not.toBeVisible({ timeout: 15_000 }) + + // Verify success notification + await expect(page.getByText('Final Grade submitted successfully')).toBeVisible({ + timeout: 10_000, + }) + + await context.close() + }) + + test('supervisor can mark a GRADED thesis as finished', async ({ browser }) => { + const context = await browser.newContext({ storageState: authStatePath('supervisor2') }) + const page = await context.newPage() + + await navigateTo(page, THESIS_URL) + + // Wait for thesis page to load + await expect(page.getByRole('heading', { name: THESIS_TITLE })).toBeVisible({ + timeout: 15_000, + }) + + // "Mark thesis as finished" button is only visible for GRADED thesis + const finishButton = page.getByRole('button', { name: 'Mark thesis as finished' }) + const isGraded = await finishButton.isVisible({ timeout: 5_000 }).catch(() => false) + + if (!isGraded) { + // Thesis is already FINISHED from a prior run — verify it has a grade displayed + await expect(page.getByText('Final Grade').first()).toBeVisible() + await context.close() + return + } + + await finishButton.click() + + // Verify success notification + await expect(page.getByText('Thesis successfully marked as finished')).toBeVisible({ + timeout: 10_000, + }) + + await context.close() + }) +}) diff --git a/client/e2e/thesis-workflow.spec.ts b/client/e2e/thesis-workflow.spec.ts new file mode 100644 index 000000000..d69b15999 --- /dev/null +++ b/client/e2e/thesis-workflow.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo, searchAndSelectMultiSelect, selectOption } from './helpers' + +test.describe('Thesis Workflow - Supervisor creates a thesis', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('supervisor can create a new thesis via the browse theses page', async ({ page }) => { + await navigateTo(page, '/theses') + await expect( + page.getByRole('heading', { name: 'Browse Theses', exact: true }), + ).toBeVisible({ timeout: 15_000 }) + + // Click "Create Thesis" button + await page.getByRole('button', { name: 'Create Thesis' }).click() + + // Modal should open + await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5_000 }) + + // Fill in the thesis creation form + await page.getByLabel('Thesis Title').fill('E2E Test Thesis: Performance Analysis') + + // Thesis Type + await selectOption(page, 'Thesis Type', /master/i) + + // Language + await selectOption(page, 'Thesis Language', /english/i) + + // Student(s) - search and select + await searchAndSelectMultiSelect(page, 'Student(s)', /student4/i) + await page.keyboard.press('Escape') + + // Supervisor(s) - search and select advisor + await searchAndSelectMultiSelect(page, 'Supervisor(s)', /advisor/i) + await page.keyboard.press('Escape') + + // Examiner - search and select self (supervisor) + await searchAndSelectMultiSelect(page, 'Examiner', /supervisor/i) + + // Research Group should be pre-filled + + // Click "Create Thesis" + const createButton = page + .getByRole('dialog') + .getByRole('button', { name: 'Create Thesis' }) + await expect(createButton).toBeEnabled({ timeout: 10_000 }) + await createButton.click() + + // Should navigate to the new thesis detail page + await expect(page).toHaveURL(/\/theses\//, { timeout: 15_000 }) + await expect( + page.getByRole('heading', { name: 'E2E Test Thesis: Performance Analysis' }), + ).toBeVisible({ timeout: 15_000 }) + }) +}) diff --git a/client/e2e/topic-workflow.spec.ts b/client/e2e/topic-workflow.spec.ts new file mode 100644 index 000000000..30e3b97d0 --- /dev/null +++ b/client/e2e/topic-workflow.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test' +import { + authStatePath, + clickMultiSelect, + fillRichTextEditor, + navigateTo, + searchAndSelectMultiSelect, +} from './helpers' + +test.describe('Topic Workflow - Supervisor creates a topic', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('supervisor can create a new topic via the manage topics page', async ({ page }) => { + await navigateTo(page, '/topics') + await expect(page.getByRole('heading', { name: 'Manage Topics', exact: true })).toBeVisible({ + timeout: 15_000, + }) + + // Click "Create Topic" button + await page.getByRole('button', { name: 'Create Topic' }).click() + + // Modal should open + await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5_000 }) + + // Fill in the topic form + await page.getByLabel('Title').fill('E2E Test Topic: Automated Testing Strategies') + + // Thesis Types (multi-select) - use force click to bypass wrapper interception + await clickMultiSelect(page, 'Thesis Types') + await page.getByRole('option', { name: /master/i }).click() + await page.keyboard.press('Escape') + + // Examiner - click to open, then select from results + await searchAndSelectMultiSelect(page, 'Examiner', /supervisor/i) + + // Supervisor(s) - click to open, then select from results + await searchAndSelectMultiSelect(page, 'Supervisor(s)', /advisor/i) + await page.keyboard.press('Escape') + + // Research Group should be pre-filled for single-group supervisors + // Problem Statement (required rich text editor) + await fillRichTextEditor( + page, + 'Problem Statement', + 'This topic explores automated testing strategies for modern web applications, focusing on E2E testing frameworks and CI integration.', + ) + + // Click "Create Topic" button in the modal + const createButton = page.getByRole('dialog').getByRole('button', { name: 'Create Topic' }) + await expect(createButton).toBeEnabled({ timeout: 10_000 }) + await createButton.click() + + // Modal should close and success notification should appear + await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15_000 }) + await expect(page.getByText('Topic created successfully')).toBeVisible({ timeout: 10_000 }) + }) +}) diff --git a/client/e2e/topics.spec.ts b/client/e2e/topics.spec.ts new file mode 100644 index 000000000..63a71f68c --- /dev/null +++ b/client/e2e/topics.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@playwright/test' +import { authStatePath, navigateTo } from './helpers' + +test.describe('Topics - Public landing page', () => { + test.use({ storageState: { cookies: [], origins: [] } }) + + test('topic search and filters work', async ({ page }) => { + await navigateTo(page, '/') + + await expect(page.getByText('Find a Thesis Topic')).toBeVisible({ timeout: 15_000 }) + // Search input is present + const searchInput = page.getByPlaceholder(/search thesis topics/i) + await expect(searchInput).toBeVisible() + + // Thesis type filter is present + await expect(page.getByText('Thesis Types').first()).toBeVisible() + + // Can switch between Open Topics and Published Topics + await page.getByText('Published Topics').click() + // The view should switch (no error) + await page.getByText('Open Topics').click() + }) + + test('can switch between list and grid view', async ({ page }) => { + await navigateTo(page, '/') + + await expect(page.getByText('Find a Thesis Topic')).toBeVisible({ timeout: 15_000 }) + // Default is list view with table headers + await expect(page.getByText('List')).toBeVisible() + await expect(page.getByText('Grid')).toBeVisible() + + // Switch to grid view + await page.getByText('Grid').click() + // Switch back to list view + await page.getByText('List').click() + // Table headers should be back + await expect(page.getByText('Title').first()).toBeVisible() + }) +}) + +test.describe('Topics - Management (Supervisor)', () => { + test.use({ storageState: authStatePath('supervisor') }) + + test('manage topics page shows topic list with actions', async ({ page }) => { + await navigateTo(page, '/topics') + + await expect(page.getByRole('heading', { name: /manage topics/i })).toBeVisible({ timeout: 15_000 }) + // Create topic button should be visible for management + await expect(page.getByRole('button', { name: 'Create Topic' })).toBeVisible() + // Table should have column headers + await expect(page.getByText('Title').first()).toBeVisible() + }) + + test('topic detail page shows topic information', async ({ page }) => { + // Navigate to a seeded open topic (Automated Code Review) + await navigateTo(page, '/topics/00000000-0000-4000-b000-000000000001') + + // Should show topic title + await expect(page.getByRole('heading', { name: /automated code review/i })).toBeVisible({ timeout: 15_000 }) + // Should show topic information sections + await expect(page.getByText(/problem statement/i).first()).toBeVisible() + // Supervisor should see applications section for this topic + }) +}) + +test.describe('Topics - Student view', () => { + test('topic detail page shows apply button for student', async ({ page }) => { + // Student viewing an open topic + await navigateTo(page, '/topics/00000000-0000-4000-b000-000000000002') + + // Should show topic title + await expect(page.getByRole('heading', { name: /continuous integration/i })).toBeVisible({ timeout: 15_000 }) + // Student should see Apply Now button + await expect(page.getByRole('link', { name: /apply now/i }).or(page.getByRole('button', { name: /apply/i }))).toBeVisible() + }) +}) diff --git a/client/package-lock.json b/client/package-lock.json index 5f618f5d5..9118e2d01 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -44,6 +44,7 @@ "@eslint/compat": "1.4.1", "@eslint/eslintrc": "3.3.3", "@eslint/js": "9.39.2", + "@playwright/test": "1.58.2", "@types/react": "19.1.12", "@types/react-avatar-editor": "13.0.4", "@types/react-dom": "19.1.9", @@ -95,13 +96,13 @@ "optional": true }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -110,9 +111,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "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, "license": "MIT", "engines": { @@ -120,9 +121,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -328,21 +329,21 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", + "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, @@ -362,12 +363,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", @@ -518,9 +519,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -546,9 +547,9 @@ } }, "node_modules/@jsonjoy.com/buffers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", - "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -579,21 +580,315 @@ "tslib": "2" } }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz", + "integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz", + "integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz", + "integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-print": "4.56.10", + "@jsonjoy.com/fs-snapshot": "4.56.10", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz", + "integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz", + "integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz", + "integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.56.10" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz", + "integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.56.10", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz", + "integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.11.0.tgz", - "integrity": "sha512-nLqSTAYwpk+5ZQIoVp7pfd/oSKNWlEdvTq2LzVA4r2wtWZg6v+5u0VgBOaDJuUfNOuw/4Ysq6glN5QKSrOCgrA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/buffers": "^1.2.0", "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0" + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -647,6 +942,23 @@ "tslib": "2" } }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -793,18 +1105,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -814,25 +1126,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -851,9 +1163,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -872,9 +1184,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -893,9 +1205,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -914,9 +1226,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -935,9 +1247,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -956,9 +1268,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -977,9 +1289,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -998,9 +1310,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -1019,9 +1331,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -1040,9 +1352,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -1061,9 +1373,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -1082,9 +1394,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -1103,99 +1415,99 @@ } }, "node_modules/@peculiar/asn1-cms": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", - "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "@peculiar/asn1-x509-attr": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-csr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", - "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", - "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", - "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-pkcs8": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", - "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", - "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-pfx": "^2.6.0", - "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "@peculiar/asn1-x509-attr": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", - "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } @@ -1213,9 +1525,9 @@ } }, "node_modules/@peculiar/asn1-x509": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", - "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", "dev": true, "license": "MIT", "dependencies": { @@ -1226,14 +1538,14 @@ } }, "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", - "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } @@ -1287,6 +1599,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "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", @@ -1308,147 +1636,183 @@ "license": "MIT" }, "node_modules/@tiptap/core": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.16.0.tgz", - "integrity": "sha512-XegRaNuoQ/guzBQU2xHxOwFXXrtoXW9tiyXDhssSqylvZmBVSlRIPNHA6ArkHBKm6ehLf6+6Y9fF3uky1yCXYQ==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz", + "integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.16.0" + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-blockquote": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.4.1.tgz", - "integrity": "sha512-8zu0LnZKl8Fl6oTGxekXhfKxVLDK2GzJwEiewZjVox5i4vVjiO19JgElv0O8mSg8MrfQckywWgXE5MLNKyuSDA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.0.tgz", + "integrity": "sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-bold": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.4.1.tgz", - "integrity": "sha512-VczYr41YirQ+NKVIGiLL8ZMoaRQW53oj2gSEUSp4fAXXhy2ARjKM3QCkjd7cuYnoDYfpdOw8gS8QRyuyW7bIFA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.0.tgz", + "integrity": "sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.0.tgz", + "integrity": "sha512-MDosUfs8Tj+nwg8RC+wTMWGkLJORXmbR6YZgbiX4hrc7G90Gopdd6kj6ht5/T8t7dLLaX7N0+DEHdUEPGED7dw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.4.1.tgz", - "integrity": "sha512-9driPr2ies5ZcNfJOekjkerF0uc5hiG0leSmQULkWMg5ucJ7ppo3fGEFdYPtw8/yBK1ZPuIiIFTUhAYhJ+SdXw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz", + "integrity": "sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.4.1" + "@tiptap/extension-list": "^3.20.0" } }, "node_modules/@tiptap/extension-code": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.4.1.tgz", - "integrity": "sha512-ZLdzl6tfp1gS4fQPpnxEYtLC50ql+depzlP7sTDy84mNdtFnGnEo19NjM402ijX2CtFQw86ieX05K8qFBzNSxw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.0.tgz", + "integrity": "sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.4.1.tgz", - "integrity": "sha512-uQM1G5YNtha4xRj1ZFYmDZWDvY4aGen0it41ZEeU5BgZnud1QBVM9OCs1A7Qya7yQaZl4Ddn7xv6Aq8X4lwJTw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz", + "integrity": "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1", - "@tiptap/pm": "^3.4.1" + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-document": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.4.1.tgz", - "integrity": "sha512-Q4Mdo0QypRvCuIW1n0gTEVi7QXgBeIQx+7721ERtlGOvwWh6MYLqKNTudZ6Ldc4pRkGJpTuxai8mmNP7eFShBw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.0.tgz", + "integrity": "sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-dropcursor": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.4.1.tgz", - "integrity": "sha512-BCK5yDpNL14A1oTfEVfPOhNt8XdDwXmS1L1eqY42NajV7U2GKfMar+OuAi/BAmrF0AF7t0has61tSqF7InmR4Q==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.0.tgz", + "integrity": "sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.20.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.0.tgz", + "integrity": "sha512-rYs4Bv5pVjqZ/2vvR6oe7ammZapkAwN51As/WDbemvYDjfOGRqK58qGauUjYZiDzPOEIzI2mxGwsZ4eJhPW4Ig==", "license": "MIT", + "optional": true, + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.4.1" + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-gapcursor": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.4.1.tgz", - "integrity": "sha512-UvN7+ZRis7vWezvk5LuisFsdlt7H73uglBABMNS+OpPQl5cZyoEyZ4195g/rYzxaIk7YSIUg0CL7ivpyPOH6JQ==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.0.tgz", + "integrity": "sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extensions": "^3.4.1" + "@tiptap/extensions": "^3.20.0" } }, "node_modules/@tiptap/extension-hard-break": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.4.1.tgz", - "integrity": "sha512-KK1LaUmgp/ODxdQ2csueIQ8s+jQclsjiC9/Upd7vzF0pzRyGdtB7qqPEXa9iijVof1ExNuxfIWzPG2YJG241DA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.0.tgz", + "integrity": "sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-heading": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.4.1.tgz", - "integrity": "sha512-N/ZLcJKmCz6WVBuNP7TLE060x3Xg9+gnlAO+kzwhpElUdMXcMnkg27PMy2jBjP0iFN7bo/hEizJ2GzuoRQ5YMw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.0.tgz", + "integrity": "sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-highlight": { @@ -1465,36 +1829,36 @@ } }, "node_modules/@tiptap/extension-horizontal-rule": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.4.1.tgz", - "integrity": "sha512-rZDci6ZER5e41qr6z8ZC8L7Ir8AaPwRfY534jH/FVuKJeg6T7TEIxUQoYFp2QZQMhr1Hb81MfmLO0RUpoIitMg==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.0.tgz", + "integrity": "sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1", - "@tiptap/pm": "^3.4.1" + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-italic": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.4.1.tgz", - "integrity": "sha512-4SAGv1N1Ywsql4te4SxR/W8DwEHtDanks9DjBjqezum2BP1HdtlpBBkh26LjbhS6I+XZqrz9CFDqjuvfXQI89Q==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.0.tgz", + "integrity": "sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-link": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.4.1.tgz", - "integrity": "sha512-SDKC4HMc42xFxIDO0t9liKSmLy6DEcueteKuIl91jUBBnP09+hv1QP94nhwxVCyO73ppHeSKxi4wXkK3lVVtPQ==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.0.tgz", + "integrity": "sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA==", "license": "MIT", "dependencies": { "linkifyjs": "^4.3.2" @@ -1504,87 +1868,87 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1", - "@tiptap/pm": "^3.4.1" + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-list": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.4.1.tgz", - "integrity": "sha512-HyPvpBoenF+e2EwIrg+/en7axIn9MEcgLzfsm0W0U0XMXY5oAyvvfFDv827Zop6NndDvbdEHLEGE8J9IEpJw3Q==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.0.tgz", + "integrity": "sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1", - "@tiptap/pm": "^3.4.1" + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/extension-list-item": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.4.1.tgz", - "integrity": "sha512-C80uU1cGci11u2yM1SCzbuqfWXsbR6bTgCiULaholHuOPWEVhlzc7HKamYxNYXifmGQkLTTZ21bRn4O11F2MVg==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.0.tgz", + "integrity": "sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.4.1" + "@tiptap/extension-list": "^3.20.0" } }, "node_modules/@tiptap/extension-list-keymap": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.4.1.tgz", - "integrity": "sha512-P4NQREr2QhUTl3BVC/Tibjh1oR/Muc0PyZ+qzPBdMv1cfATsQZR4M2FDtYOnvqpaesn+HQMnNqubS7WKLheGbA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.0.tgz", + "integrity": "sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.4.1" + "@tiptap/extension-list": "^3.20.0" } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.4.1.tgz", - "integrity": "sha512-M2ig/JD8iDBxqasCl7/QRnzoeKGo+4CgFTDbroRTbQhkxuLXhDfhWnlABjQvKDqBK465cziIA9qGwqQzePIAXw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.0.tgz", + "integrity": "sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/extension-list": "^3.4.1" + "@tiptap/extension-list": "^3.20.0" } }, "node_modules/@tiptap/extension-paragraph": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.4.1.tgz", - "integrity": "sha512-LU8cmn1jctHbr66yIbtlANt957DmryjiPwzOmQHQ6rAkiDflOmexgNeJfU3jACxjB3uT4XrNsSf5dC96Fkb94g==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.0.tgz", + "integrity": "sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-strike": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.4.1.tgz", - "integrity": "sha512-m3RJA9kq39mDUZR/i9NJVtNlbaa+7F+hv8FkNDNOyaHwn52mas7bbtNEI73/O48r7GsPhLhfzeShhGXtwRINmQ==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.0.tgz", + "integrity": "sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-subscript": { @@ -1616,16 +1980,16 @@ } }, "node_modules/@tiptap/extension-text": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.4.1.tgz", - "integrity": "sha512-6qcBjRAE4HpjbFShf9e44uBuq8jMvrumzvK4FwhAwZ7S8nyywN1WLZY3S1UQLMYJr3wZn+9IhKMb2CjBeGIPiA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.0.tgz", + "integrity": "sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1" + "@tiptap/core": "^3.20.0" } }, "node_modules/@tiptap/extension-text-align": { @@ -1668,23 +2032,23 @@ } }, "node_modules/@tiptap/extensions": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.4.1.tgz", - "integrity": "sha512-OFqcI2NwFm0pzik7yyaF/05d/j4ED3UNRVcjqwU+LdHCm7NKOChEuuyMKcqx8G6DNe9419wyKbB9fy6ykLX1lA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.0.tgz", + "integrity": "sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.4.1", - "@tiptap/pm": "^3.4.1" + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" } }, "node_modules/@tiptap/pm": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.16.0.tgz", - "integrity": "sha512-FMxZ6Tc5ONKa/EByDV8lswct6YW2lF/wn11zqXmrfBZhdG7UQPTijpSwb6TCqaO5GOHmixaIaDPj+zimUREHQA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz", + "integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.3.0", @@ -1712,14 +2076,14 @@ } }, "node_modules/@tiptap/react": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.4.1.tgz", - "integrity": "sha512-FLSHRlyk4/QG43pZcGSumipUoi55pjgrzx96n5XLNtrJnCgVZMqrFzfOYR3m2MynF9XyPIRJYqLiY2HReULniA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.0.tgz", + "integrity": "sha512-jFLNzkmn18zqefJwPje0PPd9VhZ7Oy28YHiSvSc7YpBnQIbuN/HIxZ2lrOsKyEHta0WjRZjfU5X1pGxlbcGwOA==", "license": "MIT", "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", - "fast-deep-equal": "^3.1.3", + "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "funding": { @@ -1727,12 +2091,12 @@ "url": "https://github.com/sponsors/ueberdosis" }, "optionalDependencies": { - "@tiptap/extension-bubble-menu": "^3.4.1", - "@tiptap/extension-floating-menu": "^3.4.1" + "@tiptap/extension-bubble-menu": "^3.20.0", + "@tiptap/extension-floating-menu": "^3.20.0" }, "peerDependencies": { - "@tiptap/core": "^3.4.1", - "@tiptap/pm": "^3.4.1", + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", @@ -1776,9 +2140,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "dev": true, "license": "MIT" }, @@ -1897,9 +2261,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { @@ -1935,9 +2299,9 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "dev": true, "license": "MIT", "dependencies": { @@ -2015,13 +2379,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", - "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/postcss-modules-local-by-default": { @@ -2094,13 +2458,12 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -2115,15 +2478,26 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/sockjs": { @@ -2680,9 +3054,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2716,9 +3090,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", "dependencies": { @@ -2763,6 +3137,19 @@ } } }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -2826,6 +3213,19 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3058,23 +3458,26 @@ } }, "node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", - "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/batch": { @@ -3132,27 +3535,6 @@ "ms": "2.0.0" } }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3173,16 +3555,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", @@ -3202,16 +3574,16 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -3399,9 +3771,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001765", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", - "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "version": "1.0.30001772", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", + "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", "dev": true, "funding": [ { @@ -3731,18 +4103,22 @@ } }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "dev": true, "license": "MIT" }, @@ -3845,9 +4221,9 @@ } }, "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.1.tgz", + "integrity": "sha512-gz6x+KkgNCjxq3Var03pRYLhyNfwhkKF1g/yoLgDNtFvVu0/fOLV9C8fFEZRjACp/XQLumjAYo7JVjzH3wLbxA==", "dev": true, "license": "ISC", "engines": { @@ -3996,13 +4372,13 @@ } }, "node_modules/cssnano": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", - "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.2.tgz", + "integrity": "sha512-HYOPBsNvoiFeR1eghKD5C3ASm64v9YVyJB4Ivnl2gqKoQYvjjN/G0rztvKQq8OxocUtC6sjqY8jwYngIB4AByA==", "dev": true, "license": "MIT", "dependencies": { - "cssnano-preset-default": "^7.0.9", + "cssnano-preset-default": "^7.0.10", "lilconfig": "^3.1.3" }, "engines": { @@ -4017,27 +4393,27 @@ } }, "node_modules/cssnano-preset-default": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.9.tgz", - "integrity": "sha512-tCD6AAFgYBOVpMBX41KjbvRh9c2uUjLXRyV7KHSIrwHiq5Z9o0TFfUCoM3TwVrRsRteN3sVXGNvjVNxYzkpTsA==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.10.tgz", + "integrity": "sha512-6ZBjW0Lf1K1Z+0OKUAUpEN62tSXmYChXWi2NAA0afxEVsj9a+MbcB1l5qel6BHJHmULai2fCGRthCeKSFbScpA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^5.0.1", "postcss-calc": "^10.1.1", - "postcss-colormin": "^7.0.4", - "postcss-convert-values": "^7.0.7", - "postcss-discard-comments": "^7.0.4", + "postcss-colormin": "^7.0.5", + "postcss-convert-values": "^7.0.8", + "postcss-discard-comments": "^7.0.5", "postcss-discard-duplicates": "^7.0.2", "postcss-discard-empty": "^7.0.1", "postcss-discard-overridden": "^7.0.1", "postcss-merge-longhand": "^7.0.5", - "postcss-merge-rules": "^7.0.6", + "postcss-merge-rules": "^7.0.7", "postcss-minify-font-values": "^7.0.1", "postcss-minify-gradients": "^7.0.1", - "postcss-minify-params": "^7.0.4", + "postcss-minify-params": "^7.0.5", "postcss-minify-selectors": "^7.0.5", "postcss-normalize-charset": "^7.0.1", "postcss-normalize-display-values": "^7.0.1", @@ -4045,11 +4421,11 @@ "postcss-normalize-repeat-style": "^7.0.1", "postcss-normalize-string": "^7.0.1", "postcss-normalize-timing-functions": "^7.0.1", - "postcss-normalize-unicode": "^7.0.4", + "postcss-normalize-unicode": "^7.0.5", "postcss-normalize-url": "^7.0.1", "postcss-normalize-whitespace": "^7.0.1", "postcss-ordered-values": "^7.0.2", - "postcss-reduce-initial": "^7.0.4", + "postcss-reduce-initial": "^7.0.5", "postcss-reduce-transforms": "^7.0.1", "postcss-svgo": "^7.1.0", "postcss-unique-selectors": "^7.0.4" @@ -4111,9 +4487,9 @@ "license": "CC0-1.0" }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/data-view-buffer": { @@ -4171,9 +4547,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT", "peer": true }, @@ -4220,9 +4596,9 @@ } }, "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -4237,9 +4613,9 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -4339,17 +4715,14 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/detect-node": { @@ -4559,9 +4932,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, @@ -4640,9 +5013,9 @@ } }, "node_modules/envinfo": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", - "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", "dev": true, "license": "MIT", "bin": { @@ -4667,9 +5040,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4677,9 +5050,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -4766,27 +5139,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -5114,9 +5487,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5234,9 +5607,9 @@ } }, "node_modules/express/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -5294,6 +5667,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -5341,6 +5724,24 @@ "node": ">=0.8.0" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5380,18 +5781,18 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -5588,9 +5989,9 @@ "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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, "license": "MIT", @@ -5643,6 +6044,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5713,7 +6124,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -5745,9 +6156,9 @@ } }, "node_modules/glob-to-regex.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", - "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6085,20 +6496,24 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { @@ -6244,9 +6659,9 @@ "license": "MIT" }, "node_modules/immutable": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", - "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "dev": true, "license": "MIT" }, @@ -6341,9 +6756,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", "dev": true, "license": "MIT", "engines": { @@ -6506,16 +6921,16 @@ } }, "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6548,14 +6963,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -6598,6 +7014,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -6625,9 +7057,9 @@ } }, "node_modules/is-network-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", - "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", "dev": true, "license": "MIT", "engines": { @@ -6879,25 +7311,23 @@ "license": "MIT" }, "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", "dependencies": { - "is-inside-container": "^1.0.0" + "is-docker": "^2.0.0" }, "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -6963,19 +7393,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-worker": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", @@ -7010,9 +7427,9 @@ } }, "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { @@ -7092,13 +7509,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stable-stringify/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7222,9 +7632,9 @@ } }, "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.0.tgz", + "integrity": "sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7233,10 +7643,11 @@ } }, "node_modules/less": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/less/-/less-4.4.1.tgz", - "integrity": "sha512-X9HKyiXPi0f/ed0XhgUlBeFfxrlDP3xR4M7768Zl+WXLUViuL9AOPPJP4nCV0tgRWvTYvpNmN0SFhZOQzy16PA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/less/-/less-4.5.1.tgz", + "integrity": "sha512-UKgI3/KON4u6ngSsnDADsUERqhZknsVZbnuzlRZXLQCmfC/MDld42fTydUE9B+Mla1AL6SJ/Pp6SlEFi/AVGfw==", "dev": true, + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", @@ -7558,6 +7969,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -7766,6 +8190,35 @@ "license": "MIT", "optional": true }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -7944,19 +8397,17 @@ } }, "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8210,68 +8661,6 @@ "node": ">=8" } }, - "node_modules/patch-package/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-package/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/patch-package/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/patch-package/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8341,13 +8730,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -8473,6 +8862,38 @@ "node": ">=16.0.0" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8530,13 +8951,13 @@ } }, "node_modules/postcss-colormin": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz", - "integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.5.tgz", + "integrity": "sha512-ekIBP/nwzRWhEMmIxHHbXHcMdzd1HIUzBECaj5KEdLz9DVP2HzT065sEhvOx1dkLjYW7jyD0CngThx6bpFi2fA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" @@ -8549,13 +8970,13 @@ } }, "node_modules/postcss-convert-values": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.7.tgz", - "integrity": "sha512-HR9DZLN04Xbe6xugRH6lS4ZQH2zm/bFh/ZyRkpedZozhvh+awAfbA0P36InO4fZfDhvYfNJeNvlTf1sjwGbw/A==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.8.tgz", + "integrity": "sha512-+XNKuPfkHTCEo499VzLMYn94TiL3r9YqRE3Ty+jP7UX4qjewUONey1t7CG21lrlTLN07GtGM8MqFVp86D4uKJg==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -8566,9 +8987,9 @@ } }, "node_modules/postcss-discard-comments": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.4.tgz", - "integrity": "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.5.tgz", + "integrity": "sha512-IR2Eja8WfYgN5n32vEGSctVQ1+JARfu4UH8M7bgGh1bC+xI/obsPJXaBpQF7MAByvgwZinhpHpdrmXtvVVlKcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8621,10 +9042,20 @@ } }, "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -8632,10 +9063,6 @@ "engines": { "node": "^12 || ^14 || >= 16" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.4.21" } @@ -8680,6 +9107,16 @@ "node": ">=10" } }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-loader": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.0.tgz", @@ -8757,13 +9194,13 @@ } }, "node_modules/postcss-merge-rules": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz", - "integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.7.tgz", + "integrity": "sha512-njWJrd/Ms6XViwowaaCc+/vqhPG3SmXn725AGrnl+BgTuRPEacjiLEaGq16J6XirMJbtKkTwnt67SS+e2WGoew==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "caniuse-api": "^3.0.0", "cssnano-utils": "^5.0.1", "postcss-selector-parser": "^7.1.0" @@ -8810,13 +9247,13 @@ } }, "node_modules/postcss-minify-params": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz", - "integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.5.tgz", + "integrity": "sha512-FGK9ky02h6Ighn3UihsyeAH5XmLEE2MSGH5Tc4tXMFtEDx7B+zTG6hD/+/cT+fbF7PbYojsmmWjyTwFwW1JKQQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "cssnano-utils": "^5.0.1", "postcss-value-parser": "^4.2.0" }, @@ -9056,13 +9493,13 @@ } }, "node_modules/postcss-normalize-unicode": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz", - "integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.5.tgz", + "integrity": "sha512-X6BBwiRxVaFHrb2WyBMddIeB5HBjJcAaUHyhLrM2FsxSq5TFqcHSsK7Zu1otag+o0ZphQGJewGH1tAyrD0zX1Q==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -9136,13 +9573,13 @@ } }, "node_modules/postcss-reduce-initial": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz", - "integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.5.tgz", + "integrity": "sha512-RHagHLidG8hTZcnr4FpyMB2jtgd/OcyAazjMhoy5qmWJOx1uxKh4ntk0Pb46ajKM0rkf32lRH4C8c9qQiPR6IA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "caniuse-api": "^3.0.0" }, "engines": { @@ -9169,9 +9606,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -9317,9 +9754,9 @@ } }, "node_modules/prosemirror-changeset": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", - "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", + "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==", "license": "MIT", "dependencies": { "prosemirror-transform": "^1.0.0" @@ -9357,9 +9794,9 @@ } }, "node_modules/prosemirror-gapcursor": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", - "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", "license": "MIT", "dependencies": { "prosemirror-keymap": "^1.0.0", @@ -9369,9 +9806,9 @@ } }, "node_modules/prosemirror-history": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz", - "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.2.2", @@ -9381,9 +9818,9 @@ } }, "node_modules/prosemirror-inputrules": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz", - "integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", @@ -9401,9 +9838,9 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", - "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", "license": "MIT", "dependencies": { "@types/markdown-it": "^14.0.0", @@ -9412,9 +9849,9 @@ } }, "node_modules/prosemirror-menu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", - "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz", + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==", "license": "MIT", "dependencies": { "crelt": "^1.0.0", @@ -9424,9 +9861,9 @@ } }, "node_modules/prosemirror-model": { - "version": "1.25.3", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.3.tgz", - "integrity": "sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==", + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", "dependencies": { "orderedmap": "^2.0.0" @@ -9453,9 +9890,9 @@ } }, "node_modules/prosemirror-state": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", - "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", @@ -9464,16 +9901,16 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.1.tgz", - "integrity": "sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", "license": "MIT", "dependencies": { - "prosemirror-keymap": "^1.2.2", - "prosemirror-model": "^1.25.0", - "prosemirror-state": "^1.4.3", - "prosemirror-transform": "^1.10.3", - "prosemirror-view": "^1.39.1" + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" } }, "node_modules/prosemirror-trailing-node": { @@ -9492,18 +9929,18 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz", - "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.0.tgz", - "integrity": "sha512-FatMIIl0vRHMcNc3sPy3cMw5MMyWuO1nWQxqvYpJvXAruucGvmQ2tyyjT2/Lbok77T9a/qZqBVCq4sj43V2ihw==", + "version": "1.41.6", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", + "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", @@ -9624,27 +10061,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9658,16 +10074,6 @@ "node": ">=0.10.0" } }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", @@ -9733,9 +10139,9 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -9871,6 +10277,12 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -9899,13 +10311,13 @@ } }, "node_modules/rechoir/node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -10019,19 +10431,25 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10100,9 +10518,9 @@ "license": "MIT" }, "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -10132,13 +10550,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -10162,13 +10573,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-push-apply/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -10195,9 +10599,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.92.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.92.1.tgz", - "integrity": "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", "dependencies": { @@ -10216,11 +10620,14 @@ } }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.26.0", @@ -10248,19 +10655,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -10296,25 +10690,25 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -10337,16 +10731,6 @@ "dev": true, "license": "MIT" }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -10358,22 +10742,26 @@ } }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-index/node_modules/debug": { @@ -10397,28 +10785,22 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -10426,13 +10808,6 @@ "dev": true, "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true, - "license": "ISC" - }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -10444,25 +10819,25 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/set-function-length": { @@ -10784,9 +11159,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -10794,9 +11169,9 @@ } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -10975,13 +11350,13 @@ } }, "node_modules/stylehacks": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz", - "integrity": "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.7.tgz", + "integrity": "sha512-bJkD0JkEtbRrMFtwgpJyBbFIwfDDONQ1Ov3sDLZQP8HuJ73kBOyx66H4bOcAbVWmnfLdvQ0AJwXxOMkpujcO6g==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1", + "browserslist": "^4.27.0", "postcss-selector-parser": "^7.1.0" }, "engines": { @@ -11199,9 +11574,9 @@ } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/tapable": { @@ -11219,9 +11594,9 @@ } }, "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -11351,37 +11726,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/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, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -11758,9 +12102,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -11904,9 +12248,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peer": true, "peerDependencies": { @@ -12143,14 +12487,14 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.3.tgz", - "integrity": "sha512-5kA/PzpZzDz5mNOkcNLmU1UdjGeSSxd7rt1akWpI70jMNHLASiBPRaQZn0hgyhvhawfIwSnnLfDABIxL3ueyFg==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^4.6.0", + "memfs": "^4.43.1", "mime-types": "^3.0.1", "on-finished": "^2.4.1", "range-parser": "^1.2.1", @@ -12173,12 +12517,20 @@ } }, "node_modules/webpack-dev-middleware/node_modules/memfs": { - "version": "4.38.2", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.38.2.tgz", - "integrity": "sha512-FpWsVHpAkoSh/LfY1BgAl72BVd374ooMRtDi2VqzBycX4XEfvC0XKACCe0C9VRZoYq5viuoyTv6lYXZ/Q7TrLQ==", + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", + "integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@jsonjoy.com/fs-core": "4.56.10", + "@jsonjoy.com/fs-fsa": "4.56.10", + "@jsonjoy.com/fs-node": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-to-fsa": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-print": "4.56.10", + "@jsonjoy.com/fs-snapshot": "4.56.10", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", @@ -12186,12 +12538,12 @@ "tree-dump": "^1.0.3", "tslib": "^2.0.0" }, - "engines": { - "node": ">= 4.0.0" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, "node_modules/webpack-dev-middleware/node_modules/mime-db": { @@ -12205,16 +12557,20 @@ } }, "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/webpack-dev-server": { @@ -12313,6 +12669,38 @@ "node": ">= 6" } }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/webpack-dev-server/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -12327,9 +12715,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": { @@ -12364,9 +12752,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { @@ -12514,13 +12902,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-builtin-type/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", @@ -12541,9 +12922,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -12624,14 +13005,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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, "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yn": { diff --git a/client/package.json b/client/package.json index c625315d9..fa599de21 100644 --- a/client/package.json +++ b/client/package.json @@ -30,6 +30,9 @@ "prettier:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"", "prettier:write": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", "update": "ncu -i --format group", + "e2e": "playwright test", + "e2e:ui": "playwright test --ui", + "e2e:headed": "playwright test --headed", "prepare": "cd .. && husky client/.husky" }, "overrides": { @@ -74,6 +77,7 @@ "@eslint/compat": "1.4.1", "@eslint/eslintrc": "3.3.3", "@eslint/js": "9.39.2", + "@playwright/test": "1.58.2", "@types/react": "19.1.12", "@types/react-avatar-editor": "13.0.4", "@types/react-dom": "19.1.9", @@ -93,6 +97,7 @@ "html-webpack-plugin": "5.6.6", "husky": "9.1.7", "mini-css-extract-plugin": "2.10.0", + "patch-package": "8.0.1", "postcss": "8.5.6", "postcss-loader": "8.2.0", "postcss-preset-mantine": "1.18.0", @@ -108,7 +113,6 @@ "webpack-bundle-analyzer": "4.10.2", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.3", - "patch-package": "8.0.1", "webpackbar": "7.0.0" } } diff --git a/client/playwright.config.ts b/client/playwright.config.ts new file mode 100644 index 000000000..92d46621c --- /dev/null +++ b/client/playwright.config.ts @@ -0,0 +1,39 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 1, + workers: process.env.CI ? 2 : 8, + reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : [['html', { open: 'never' }]], + timeout: 60_000, + expect: { + timeout: 15_000, + }, + use: { + baseURL: process.env.CLIENT_URL ?? 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'on-first-retry', + }, + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + storageState: 'e2e/.auth/student.json', + }, + dependencies: ['setup'], + }, + ], + webServer: process.env.CI + ? undefined + : { + command: 'npm run dev', + url: 'http://localhost:3000', + reuseExistingServer: true, + timeout: 120_000, + }, +}) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a1e08f43c..5a9dd7514 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -186,6 +186,123 @@ npm run dev Client is served at http://localhost:3000. +## E2E Tests (Playwright) + +The project includes end-to-end tests using [Playwright](https://playwright.dev/) that verify the client application works correctly across all user roles. Tests run against the full dev stack (PostgreSQL, Keycloak, server with seed data, client). + +### Prerequisites + +The E2E tests require all dev services to be running: + +1. **PostgreSQL + Keycloak**: `docker compose up -d` +2. **Server** (dev profile with seed data): `cd server && ./gradlew bootRun --args='--spring.profiles.active=dev'` +3. **Client** (dev server): `cd client && npm run dev` +4. **Install Playwright browsers** (first time only): `cd client && npx playwright install chromium` + +### Running E2E Tests + +#### One-command local run (recommended) + +The `execute-e2e-local.sh` script in the project root starts all required services automatically and runs the tests. It is idempotent — it detects already-running services and reuses them, so it can be executed repeatedly. + +```bash +# Headless (default) +./execute-e2e-local.sh + +# Interactive Playwright UI +./execute-e2e-local.sh --ui + +# Headed browser (watch tests run) +./execute-e2e-local.sh --headed + +# Stop all services started by the script +./execute-e2e-local.sh --stop +``` + +#### Manual run (when services are already running) + +```bash +cd client + +# Headless +npm run e2e + +# Interactive Playwright UI +npm run e2e:ui + +# Headed browser +npm run e2e:headed +``` + +#### Run a single test file + +```bash +cd client +npx playwright test e2e/auth.spec.ts +``` + +### Test Structure + +Tests are located in `client/e2e/` and authenticate via the Keycloak login form using the seeded test users (password = username). Auth state is cached in `e2e/.auth/` and reused across tests. Shared helpers (`helpers.ts`) provide utilities for navigation, Mantine component interaction (select, multi-select, rich text editor), and test data generation. + +| File | Description | +|------|-------------| +| `auth.setup.ts` | Authenticates all test users (student, student2, student3, advisor, advisor2, supervisor, supervisor2, admin) via Keycloak and caches their session state | +| `auth.spec.ts` | Keycloak redirect for unauthenticated users, role-based navigation item visibility for all 5 access levels | +| `navigation.spec.ts` | Public page rendering (landing page, about, footer), sidebar navigation flow, route access per role | +| `dashboard.spec.ts` | Dashboard sections per role (My Theses, My Applications) | +| `topics.spec.ts` | Public topic browsing with search, filters, and list/grid toggle; supervisor management view; student apply button | +| `applications.spec.ts` | Student application stepper form, pre-selected topic flow, advisor and supervisor review page access | +| `theses.spec.ts` | Browse view per role, theses overview, thesis detail page sections, student viewing own thesis | +| `interviews.spec.ts` | Supervisor interview overview and process detail, advisor access, student access denied | +| `presentations.spec.ts` | Student and supervisor presentations page, public presentation detail access | +| `settings.spec.ts` | My Information and Notification Settings tabs for student and advisor | +| `research-groups.spec.ts` | Admin research group CRUD with search filtering, supervisor group access, student access denied | +| **Workflow Tests** | | +| `topic-workflow.spec.ts` | Supervisor creates a new topic end-to-end: fills title, thesis types, examiner, supervisor, problem statement | +| `thesis-workflow.spec.ts` | Supervisor creates a new thesis end-to-end: fills title, type, language, student, supervisor, examiner | +| `application-workflow.spec.ts` | Student submits an application through the full stepper: topic selection, student info, file uploads, motivation | +| `presentation-workflow.spec.ts` | Student creates a presentation draft for a submitted thesis: type, visibility, location, language, date/time | +| `proposal-feedback-workflow.spec.ts` | Advisor submits proposal feedback on a thesis in PROPOSAL state: opens feedback dialog, enters comment, submits | +| `application-review-workflow.spec.ts` | Advisor rejects and accepts NOT_ASSESSED applications: reject with reason, accept with pre-filled thesis details | +| `thesis-grading-workflow.spec.ts` | Sequential thesis grading: advisor submits assessment, supervisor submits final grade, supervisor marks thesis as finished | +| `interview-workflow.spec.ts` | Supervisor scores an interviewee with notes, opens add slot modal on interview process page | + +### Tested Roles + +Every major page is tested with appropriate roles to verify access control: + +- **Unauthenticated** — public pages are accessible, protected routes redirect to Keycloak login +- **Student** — dashboard, submit application, browse theses, settings, presentations; cannot access management pages +- **Advisor** — dashboard, review applications, manage topics, theses overview, interviews, settings +- **Supervisor** — same as advisor (management view); additional thesis detail assertions +- **Admin** — all pages including research groups management + +### Coverage + +The E2E tests focus on page accessibility, content rendering, and role-based access control. The table below summarizes what is currently covered and what is not. + +| Area | Covered | Not yet covered | +|------|---------|-----------------| +| **Authentication & RBAC** | Keycloak redirect, nav item visibility per role, access denied for unauthorized roles | Token refresh, session expiry, logout | +| **Topics** | Public browsing, search, filters, list/grid toggle, management view, student apply button, **creating a topic end-to-end** | Editing/closing topics, draft topics | +| **Applications** | Stepper form rendering, pre-selected topic, advisor/supervisor review page access, **submitting an application end-to-end**, **accepting and rejecting applications** | — | +| **Theses** | Browse per role, overview page, detail page sections, student own thesis, **creating a thesis end-to-end**, **submitting proposal feedback**, **assessment → final grade → mark as finished** | Comments | +| **Interviews** | Supervisor overview and process detail, advisor access, student denied, **scoring interviewees with notes**, **add slot modal** | Creating interview processes, booking slots | +| **Presentations** | Page access per role, public presentation detail, **creating a presentation draft** | Calendar integration | +| **Settings** | Tab rendering per role | Editing profile information, notification preferences | +| **Research Groups** | Admin CRUD page, search filtering, student denied | Creating/editing groups, member management | +| **Dashboard** | Section visibility per role (My Theses, My Applications) | Dashboard data accuracy, links to detail pages | +| **Navigation** | Public pages, sidebar flow, header logo, footer links, unknown routes | Mobile/responsive layout, deep linking | + +**In summary:** The tests cover page rendering/access control across all roles and key end-to-end workflows including topic creation, thesis creation, application submission, presentation scheduling, proposal feedback, application accept/reject, thesis grading (assessment → grade → finish), and interview scoring. + +### CI Integration + +E2E tests run automatically in CI via the `e2e_tests.yml` reusable workflow, which is called from `dev.yml` on PRs and pushes to develop/main. The CI workflow spins up PostgreSQL and Keycloak as service containers, starts the server with the dev profile, builds and serves the client, and runs the Playwright tests. + +Test artifacts (screenshots, traces, videos) are uploaded on failure and available in the GitHub Actions artifacts. + ## Postman Collection A ready-to-use Postman Collection is included: [`TUMApply API.postman_collection.json`](./Thesis%20Management%20API.postman_collection.json). diff --git a/execute-e2e-local.sh b/execute-e2e-local.sh new file mode 100755 index 000000000..d91f78aca --- /dev/null +++ b/execute-e2e-local.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================ +# Local E2E test runner +# +# Usage: +# ./execute-e2e-local.sh Run E2E tests (starts services if needed) +# ./execute-e2e-local.sh --stop Stop all services started by this script +# ./execute-e2e-local.sh --ui Run tests in interactive Playwright UI mode +# ./execute-e2e-local.sh --headed Run tests in headed browser mode +# ============================================================================ + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +CLIENT_DIR="$ROOT_DIR/client" +SERVER_DIR="$ROOT_DIR/server" +PID_DIR="$ROOT_DIR/.e2e-pids" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${CYAN}[e2e]${NC} $*"; } +ok() { echo -e "${GREEN}[e2e]${NC} $*"; } +warn() { echo -e "${YELLOW}[e2e]${NC} $*"; } +err() { echo -e "${RED}[e2e]${NC} $*"; } + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +is_port_open() { + lsof -iTCP:"$1" -sTCP:LISTEN -t >/dev/null 2>&1 +} + +wait_for_url() { + local url="$1" label="$2" max_wait="${3:-120}" + log "Waiting for $label ..." + for i in $(seq 1 "$max_wait"); do + if curl -sf "$url" >/dev/null 2>&1; then + ok "$label is ready (${i}s)" + return 0 + fi + sleep 1 + done + err "$label did not start within ${max_wait}s" + return 1 +} + +save_pid() { + mkdir -p "$PID_DIR" + echo "$2" > "$PID_DIR/$1.pid" +} + +read_pid() { + local f="$PID_DIR/$1.pid" + [[ -f "$f" ]] && cat "$f" || echo "" +} + +kill_pid() { + local pid + pid=$(read_pid "$1") + if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + ok "Stopped $1 (PID $pid)" + fi + rm -f "$PID_DIR/$1.pid" +} + +# --------------------------------------------------------------------------- +# Stop command +# --------------------------------------------------------------------------- + +stop_all() { + log "Stopping E2E services..." + kill_pid "client" + kill_pid "server" + (cd "$ROOT_DIR" && docker compose stop 2>/dev/null) || true + rm -rf "$PID_DIR" + ok "All services stopped." + exit 0 +} + +# --------------------------------------------------------------------------- +# Parse args +# --------------------------------------------------------------------------- + +PLAYWRIGHT_ARGS=() +for arg in "$@"; do + case "$arg" in + --stop) stop_all ;; + --ui) PLAYWRIGHT_ARGS+=(--ui) ;; + --headed) PLAYWRIGHT_ARGS+=(--headed) ;; + *) PLAYWRIGHT_ARGS+=("$arg") ;; + esac +done + +# --------------------------------------------------------------------------- +# 1. Docker services (PostgreSQL + Keycloak + CalDAV) +# --------------------------------------------------------------------------- + +log "Starting Docker services..." +(cd "$ROOT_DIR" && docker compose up -d) 2>&1 | while IFS= read -r line; do echo " $line"; done + +# Wait for Keycloak by checking the realm endpoint (health endpoint is not available in dev mode) +wait_for_url "http://localhost:8081/realms/thesis-management" "Keycloak" 90 + +# --------------------------------------------------------------------------- +# 2. Server (Spring Boot with dev profile) +# --------------------------------------------------------------------------- + +if is_port_open 8080; then + ok "Server already running on port 8080" +else + log "Starting server (dev profile)..." + (cd "$SERVER_DIR" && exec ./gradlew bootRun --args='--spring.profiles.active=dev' \ + > "$ROOT_DIR/.e2e-server.log" 2>&1) & + save_pid "server" $! +fi + +# --------------------------------------------------------------------------- +# 3. Client dev server +# --------------------------------------------------------------------------- + +if is_port_open 3000; then + ok "Client already running on port 3000" +else + log "Starting client dev server..." + (cd "$CLIENT_DIR" && exec npx webpack serve --env NODE_ENV=development \ + > "$ROOT_DIR/.e2e-client.log" 2>&1) & + save_pid "client" $! +fi + +# --------------------------------------------------------------------------- +# 4. Wait for everything to be ready +# --------------------------------------------------------------------------- + +wait_for_url "http://localhost:8080/api/actuator/health" "Server" 180 +wait_for_url "http://localhost:3000" "Client" 60 + +# --------------------------------------------------------------------------- +# 5. Run Playwright tests +# --------------------------------------------------------------------------- + +echo "" +log "Running Playwright E2E tests..." +echo "" + +cd "$CLIENT_DIR" +npx playwright test "${PLAYWRIGHT_ARGS[@]+"${PLAYWRIGHT_ARGS[@]}"}" +EXIT_CODE=$? + +echo "" +if [[ $EXIT_CODE -eq 0 ]]; then + ok "All E2E tests passed!" +else + err "Some E2E tests failed (exit code $EXIT_CODE)" + warn "View report: cd client && npx playwright show-report" +fi + +warn "Services are still running. Use './execute-e2e-local.sh --stop' to stop them." +exit $EXIT_CODE