فهرست منبع

✨ feat(test-utils): 新增共享测试工具包

- 创建完整的测试工具包结构,包含服务器端和客户端测试工具
- 提供客户端测试工具:React Query测试工具、测试渲染器、路由测试工具
- 提供服务器端测试工具:数据库测试工具、认证测试工具、服务模拟工具
- 包含集成测试数据库工具,支持真实PostgreSQL数据库测试
- 提供测试数据工厂,方便创建测试用户和角色数据
- 添加服务mock和stub工具,支持HTTP服务、认证服务、邮件服务等模拟
- 包含测试环境设置和全局配置,支持统一的测试环境管理
- 提供TypeScript配置和完整的包配置,支持模块化导入
yourname 1 ماه پیش
والد
کامیت
a4d808f4b8

+ 51 - 0
packages/test-utils/package.json

@@ -0,0 +1,51 @@
+{
+  "name": "@d8d/test-utils",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "D8D Shared Test Utilities",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "import": "./src/index.ts",
+      "require": "./src/index.ts",
+      "types": "./src/index.ts"
+    },
+    "./server": {
+      "import": "./src/server/index.ts",
+      "require": "./src/server/index.ts",
+      "types": "./src/server/index.ts"
+    },
+    "./client": {
+      "import": "./src/client/index.ts",
+      "require": "./src/client/index.ts",
+      "types": "./src/client/index.ts"
+    }
+  },
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "typecheck": "tsc --noEmit",
+    "test": "vitest",
+    "test:run": "vitest run"
+  },
+  "dependencies": {
+    "@testing-library/react": "^13.4.0",
+    "@testing-library/jest-dom": "^6.4.2",
+    "@testing-library/user-event": "^14.5.2",
+    "@types/node": "^20.10.5"
+  },
+  "devDependencies": {
+    "typescript": "^5.8.3",
+    "vitest": "^2.0.5"
+  },
+  "peerDependencies": {
+    "@d8d/server": "workspace:*",
+    "hono": "^4.8.5",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0"
+  },
+  "files": [
+    "src"
+  ]
+}

+ 91 - 0
packages/test-utils/src/client/test-query.tsx

@@ -0,0 +1,91 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactNode } from 'react';
+import { vi } from 'vitest';
+
+/**
+ * 创建测试用的QueryClient(带默认配置)
+ */
+export function createTestQueryClient(options = {}) {
+  return new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        gcTime: 0,
+        staleTime: 0,
+      },
+      mutations: {
+        retry: false,
+      },
+    },
+    ...options
+  });
+}
+
+/**
+ * QueryProvider包装组件
+ */
+export function TestQueryProvider({
+  children,
+  client
+}: {
+  children: ReactNode;
+  client?: QueryClient
+}) {
+  const queryClient = client || createTestQueryClient();
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  );
+}
+
+/**
+ * Mock查询Hook
+ */
+export function mockUseQuery(data: any, isLoading = false, error: any = null) {
+  return vi.fn().mockReturnValue({
+    data,
+    isLoading,
+    isError: !!error,
+    error,
+    isSuccess: !isLoading && !error,
+    refetch: vi.fn(),
+  });
+}
+
+/**
+ * Mock变更Hook
+ */
+export function mockUseMutation() {
+  return vi.fn().mockReturnValue({
+    mutate: vi.fn(),
+    mutateAsync: vi.fn().mockResolvedValue({}),
+    isLoading: false,
+    isError: false,
+    error: null,
+    isSuccess: false,
+    reset: vi.fn(),
+  });
+}
+
+/**
+ * 等待查询完成
+ */
+export async function waitForQueryToFinish(delay = 100) {
+  await new Promise(resolve => setTimeout(resolve, delay));
+}
+
+/**
+ * 模拟网络错误
+ */
+export function mockNetworkError() {
+  return new Error('Network error');
+}
+
+/**
+ * 模拟服务器错误
+ */
+export function mockServerError() {
+  return new Error('Server error');
+}

+ 80 - 0
packages/test-utils/src/client/test-render.tsx

@@ -0,0 +1,80 @@
+import { ReactNode } from 'react';
+import { BrowserRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ThemeProvider } from 'next-themes';
+import { AuthProvider } from '../../../src/client/admin/hooks/AuthProvider';
+import { vi } from 'vitest';
+
+/**
+ * 创建测试用的QueryClient
+ */
+export function createTestQueryClient() {
+  return new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        gcTime: 0,
+      },
+      mutations: {
+        retry: false,
+      },
+    }
+  });
+}
+
+/**
+ * 测试渲染器的包装组件
+ */
+export function TestWrapper({ children }: { children: ReactNode }) {
+  const queryClient = createTestQueryClient();
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      <ThemeProvider attribute="class" defaultTheme="light">
+        <BrowserRouter>
+          {children}
+        </BrowserRouter>
+      </ThemeProvider>
+    </QueryClientProvider>
+  );
+}
+
+/**
+ * 专门用于admin页面的测试包装器,包含AuthProvider
+ */
+export function AdminTestWrapper({ children }: { children: ReactNode }) {
+  const queryClient = createTestQueryClient();
+
+  // Mock localStorage for tests
+  const localStorageMock = {
+    getItem: vi.fn(() => null),
+    setItem: vi.fn(),
+    removeItem: vi.fn(),
+    clear: vi.fn(),
+  };
+
+  // Set up localStorage mock
+  Object.defineProperty(window, 'localStorage', {
+    value: localStorageMock,
+    writable: true,
+  });
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      <ThemeProvider attribute="class" defaultTheme="light">
+        <BrowserRouter>
+          <AuthProvider>
+            {children}
+          </AuthProvider>
+        </BrowserRouter>
+      </ThemeProvider>
+    </QueryClientProvider>
+  );
+}
+
+/**
+ * 等待组件更新完成
+ */
+export async function waitForUpdate(delay = 0) {
+  await new Promise(resolve => setTimeout(resolve, delay));
+}

+ 47 - 0
packages/test-utils/src/client/test-router.tsx

@@ -0,0 +1,47 @@
+import { ReactNode } from 'react';
+import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom';
+import { vi } from 'vitest';
+
+/**
+ * 测试路由器的包装组件
+ */
+export function TestRouter({
+  children,
+  initialPath = '/',
+  routes = []
+}: {
+  children: ReactNode;
+  initialPath?: string;
+  routes?: Array<{ path: string; element: ReactNode }>;
+}) {
+  return (
+    <MemoryRouter initialEntries={[initialPath]}>
+      <Routes>
+        {routes.map((route, index) => (
+          <Route key={index} path={route.path} element={route.element} />
+        ))}
+        <Route path="*" element={children} />
+      </Routes>
+    </MemoryRouter>
+  );
+}
+
+/**
+ * 获取当前路由位置的Hook
+ */
+export function useTestLocation() {
+  const location = useLocation();
+  return location;
+}
+
+/**
+ * 创建测试导航函数
+ */
+export function createTestNavigation() {
+  return {
+    navigate: vi.fn(),
+    goBack: vi.fn(),
+    goForward: vi.fn(),
+    replace: vi.fn()
+  };
+}

+ 44 - 0
packages/test-utils/src/index.ts

@@ -0,0 +1,44 @@
+/**
+ * D8D Shared Test Utilities
+ *
+ * 统一的测试工具包,为服务器和客户端测试提供共享工具函数
+ */
+
+// 通用测试工具
+export * from './test-utils.js';
+export * from './setup.js';
+
+// 服务器测试工具
+export * from './server/index.js';
+
+// 客户端测试工具
+export * from './client/index.js';
+
+/**
+ * 测试工具包配置接口
+ */
+export interface TestUtilsConfig {
+  /** 测试环境配置 */
+  environment: 'test' | 'development' | 'production';
+  /** 是否启用详细日志 */
+  verbose?: boolean;
+  /** 测试数据库配置 */
+  database?: {
+    host: string;
+    port: number;
+    name: string;
+  };
+}
+
+/**
+ * 测试工具包基础配置
+ */
+export const defaultTestConfig: TestUtilsConfig = {
+  environment: 'test',
+  verbose: false,
+  database: {
+    host: 'localhost',
+    port: 5432,
+    name: 'test_db'
+  }
+};

+ 55 - 0
packages/test-utils/src/server/index.ts

@@ -0,0 +1,55 @@
+/**
+ * 服务器端测试工具
+ *
+ * 为 Hono 服务器和 API 测试提供工具函数
+ */
+
+/**
+ * 集成测试数据库工具
+ */
+export * from './integration-test-db.js';
+
+/**
+ * 集成测试工具函数
+ */
+export * from './integration-test-utils.js';
+
+/**
+ * 认证测试工具
+ */
+export * from './test-auth.js';
+
+/**
+ * 数据库测试工具
+ */
+export * from './test-db.js';
+
+/**
+ * 服务模拟工具
+ */
+export * from './service-mocks.js';
+
+/**
+ * 服务存根工具
+ */
+export * from './service-stubs.js';
+
+/**
+ * 服务器测试配置
+ */
+export interface ServerTestConfig {
+  /** API 基础路径 */
+  baseUrl: string;
+  /** 认证令牌 */
+  authToken?: string;
+  /** 请求超时时间(毫秒) */
+  timeout?: number;
+}
+
+/**
+ * 默认服务器测试配置
+ */
+export const defaultServerTestConfig: ServerTestConfig = {
+  baseUrl: 'http://localhost:8080',
+  timeout: 5000
+};

+ 99 - 0
packages/test-utils/src/server/integration-test-db.ts

@@ -0,0 +1,99 @@
+import { DataSource } from 'typeorm';
+import { beforeEach, afterEach } from 'vitest';
+import { UserEntity } from '@d8d/server/modules/users/user.entity';
+import { Role } from '@d8d/server/modules/users/role.entity';
+import { AppDataSource } from '@d8d/server/data-source';
+
+/**
+ * 集成测试数据库工具类 - 使用真实PostgreSQL数据库
+ */
+export class IntegrationTestDatabase {
+  /**
+   * 清理集成测试数据库
+   */
+  static async cleanup(): Promise<void> {
+    if (AppDataSource.isInitialized) {
+      await AppDataSource.destroy();
+    }
+  }
+
+  /**
+   * 获取当前数据源
+   */
+  static async getDataSource(): Promise<DataSource> {
+    if(!AppDataSource.isInitialized) {
+      await AppDataSource.initialize();
+    }
+    return AppDataSource
+  }
+}
+
+/**
+ * 测试数据工厂类
+ */
+export class TestDataFactory {
+  /**
+   * 创建测试用户数据
+   */
+  static createUserData(overrides: Partial<UserEntity> = {}): Partial<UserEntity> {
+    const timestamp = Date.now();
+    return {
+      username: `testuser_${timestamp}`,
+      password: 'TestPassword123!',
+      email: `test_${timestamp}@example.com`,
+      phone: `138${timestamp.toString().slice(-8)}`,
+      nickname: `Test User ${timestamp}`,
+      name: `Test Name ${timestamp}`,
+      isDisabled: 0,
+      isDeleted: 0,
+      ...overrides
+    };
+  }
+
+  /**
+   * 创建测试角色数据
+   */
+  static createRoleData(overrides: Partial<Role> = {}): Partial<Role> {
+    const timestamp = Date.now();
+    return {
+      name: `test_role_${timestamp}`,
+      description: `Test role description ${timestamp}`,
+      ...overrides
+    };
+  }
+
+  /**
+   * 在数据库中创建测试用户
+   */
+  static async createTestUser(dataSource: DataSource, overrides: Partial<UserEntity> = {}): Promise<UserEntity> {
+    const userData = this.createUserData(overrides);
+    const userRepository = dataSource.getRepository(UserEntity);
+
+    const user = userRepository.create(userData);
+    return await userRepository.save(user);
+  }
+
+  /**
+   * 在数据库中创建测试角色
+   */
+  static async createTestRole(dataSource: DataSource, overrides: Partial<Role> = {}): Promise<Role> {
+    const roleData = this.createRoleData(overrides);
+    const roleRepository = dataSource.getRepository(Role);
+
+    const role = roleRepository.create(roleData);
+    return await roleRepository.save(role);
+  }
+}
+
+/**
+ * 集成测试数据库生命周期钩子
+ */
+export function setupIntegrationDatabaseHooks() {
+  beforeEach(async () => {
+    await IntegrationTestDatabase.getDataSource();
+  });
+
+  afterEach(async () => {
+    await IntegrationTestDatabase.cleanup();
+  });
+}

+ 75 - 0
packages/test-utils/src/server/integration-test-utils.ts

@@ -0,0 +1,75 @@
+
+import { IntegrationTestDatabase } from './integration-test-db';
+import { UserEntity } from '@d8d/server/modules/users/user.entity';
+
+
+
+/**
+ * 集成测试断言工具
+ */
+export class IntegrationTestAssertions {
+  /**
+   * 断言响应状态码
+   */
+  static expectStatus(response: { status: number }, expectedStatus: number): void {
+    if (response.status !== expectedStatus) {
+      throw new Error(`Expected status ${expectedStatus}, but got ${response.status}`);
+    }
+  }
+
+  /**
+   * 断言响应包含特定字段
+   */
+  static expectResponseToHave(response: { data: any }, expectedFields: Record<string, any>): void {
+    for (const [key, value] of Object.entries(expectedFields)) {
+      if (response.data[key] !== value) {
+        throw new Error(`Expected field ${key} to be ${value}, but got ${response.data[key]}`);
+      }
+    }
+  }
+
+  /**
+   * 断言响应包含特定结构
+   */
+  static expectResponseStructure(response: { data: any }, structure: Record<string, any>): void {
+    for (const key of Object.keys(structure)) {
+      if (!(key in response.data)) {
+        throw new Error(`Expected response to have key: ${key}`);
+      }
+    }
+  }
+
+  /**
+   * 断言用户存在于数据库中
+   */
+  static async expectUserToExist(username: string): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const userRepository = dataSource.getRepository(UserEntity);
+    const user = await userRepository.findOne({ where: { username } });
+
+    if (!user) {
+      throw new Error(`Expected user ${username} to exist in database`);
+    }
+  }
+
+  /**
+   * 断言用户不存在于数据库中
+   */
+  static async expectUserNotToExist(username: string): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const userRepository = dataSource.getRepository(UserEntity);
+    const user = await userRepository.findOne({ where: { username } });
+
+    if (user) {
+      throw new Error(`Expected user ${username} not to exist in database`);
+    }
+  }
+}

+ 164 - 0
packages/test-utils/src/server/service-mocks.ts

@@ -0,0 +1,164 @@
+import { vi } from 'vitest';
+
+/**
+ * 创建HTTP服务mock
+ */
+export function createHttpServiceMock() {
+  return {
+    get: vi.fn().mockResolvedValue({ data: {}, status: 200 }),
+    post: vi.fn().mockResolvedValue({ data: {}, status: 201 }),
+    put: vi.fn().mockResolvedValue({ data: {}, status: 200 }),
+    delete: vi.fn().mockResolvedValue({ data: {}, status: 204 }),
+    patch: vi.fn().mockResolvedValue({ data: {}, status: 200 }),
+  };
+}
+
+/**
+ * 创建认证服务mock
+ */
+export function createAuthServiceMock() {
+  return {
+    verifyToken: vi.fn().mockResolvedValue({ userId: 1, username: 'testuser' }),
+    generateToken: vi.fn().mockReturnValue('mock-jwt-token'),
+    refreshToken: vi.fn().mockResolvedValue({ accessToken: 'new-token', refreshToken: 'new-refresh-token' }),
+    invalidateToken: vi.fn().mockResolvedValue(undefined),
+  };
+}
+
+/**
+ * 创建邮件服务mock
+ */
+export function createEmailServiceMock() {
+  return {
+    sendWelcomeEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendPasswordResetEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendNotification: vi.fn().mockResolvedValue({ success: true }),
+  };
+}
+
+/**
+ * 创建文件存储服务mock
+ */
+export function createStorageServiceMock() {
+  return {
+    uploadFile: vi.fn().mockResolvedValue({ url: 'https://example.com/file.jpg', key: 'file-key' }),
+    deleteFile: vi.fn().mockResolvedValue({ success: true }),
+    getFileUrl: vi.fn().mockReturnValue('https://example.com/file.jpg'),
+    listFiles: vi.fn().mockResolvedValue([]),
+  };
+}
+
+/**
+ * 创建支付服务mock
+ */
+export function createPaymentServiceMock() {
+  return {
+    createPaymentIntent: vi.fn().mockResolvedValue({ clientSecret: 'pi_mock_secret', id: 'pi_mock_id' }),
+    confirmPayment: vi.fn().mockResolvedValue({ success: true, transactionId: 'txn_mock_id' }),
+    refundPayment: vi.fn().mockResolvedValue({ success: true, refundId: 'ref_mock_id' }),
+  };
+}
+
+/**
+ * 创建短信服务mock
+ */
+export function createSmsServiceMock() {
+  return {
+    sendVerificationCode: vi.fn().mockResolvedValue({ success: true, messageId: 'sms_mock_id' }),
+    sendNotification: vi.fn().mockResolvedValue({ success: true, messageId: 'sms_mock_id' }),
+  };
+}
+
+/**
+ * 创建第三方API服务mock
+ */
+export function createThirdPartyApiMock() {
+  return {
+    call: vi.fn().mockResolvedValue({ success: true, data: {} }),
+    validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }),
+    webhook: vi.fn().mockResolvedValue({ received: true }),
+  };
+}
+
+/**
+ * 模拟网络延迟
+ */
+export function mockNetworkDelay(delayMs: number) {
+  return new Promise(resolve => setTimeout(resolve, delayMs));
+}
+
+/**
+ * 模拟HTTP错误响应
+ */
+export function mockHttpError(status: number, message: string) {
+  return {
+    response: {
+      status,
+      data: { error: message }
+    }
+  };
+}
+
+/**
+ * 模拟超时错误
+ */
+export function mockTimeoutError() {
+  return new Error('Request timeout');
+}
+
+/**
+ * 模拟网络断开错误
+ */
+export function mockNetworkError() {
+  return new Error('Network error');
+}
+
+/**
+ * 服务mock工具类
+ */
+export class ServiceMocks {
+  static http = createHttpServiceMock();
+  static auth = createAuthServiceMock();
+  static email = createEmailServiceMock();
+  static storage = createStorageServiceMock();
+  static payment = createPaymentServiceMock();
+  static sms = createSmsServiceMock();
+  static thirdParty = createThirdPartyApiMock();
+
+  /**
+   * 重置所有mock
+   */
+  static resetAll() {
+    Object.values(this).forEach(service => {
+      if (service && typeof service === 'object') {
+        Object.values(service).forEach(mock => {
+          if (mock && typeof mock === 'function' && 'mockClear' in mock) {
+            (mock as any).mockClear();
+          }
+        });
+      }
+    });
+  }
+
+  /**
+   * 设置所有mock为成功状态
+   */
+  static setupForSuccess() {
+    this.resetAll();
+    // 所有mock默认已经是成功状态
+  }
+
+  /**
+   * 设置所有mock为失败状态
+   */
+  static setupForFailure() {
+    this.resetAll();
+    Object.values(this.http).forEach(mock => mock.mockRejectedValue(mockNetworkError()));
+    Object.values(this.auth).forEach(mock => mock.mockRejectedValue(new Error('Auth failed')));
+    Object.values(this.email).forEach(mock => mock.mockRejectedValue(new Error('Email failed')));
+    Object.values(this.storage).forEach(mock => mock.mockRejectedValue(new Error('Storage failed')));
+    Object.values(this.payment).forEach(mock => mock.mockRejectedValue(new Error('Payment failed')));
+    Object.values(this.sms).forEach(mock => mock.mockRejectedValue(new Error('SMS failed')));
+    Object.values(this.thirdParty).forEach(mock => mock.mockRejectedValue(new Error('Third party failed')));
+  }
+}

+ 158 - 0
packages/test-utils/src/server/service-stubs.ts

@@ -0,0 +1,158 @@
+import { vi } from 'vitest';
+
+/**
+ * 创建模拟的用户服务
+ */
+export function createMockUserService() {
+  return {
+    getUserById: vi.fn().mockResolvedValue(null),
+    getUserByUsername: vi.fn().mockResolvedValue(null),
+    getUserByEmail: vi.fn().mockResolvedValue(null),
+    createUser: vi.fn().mockResolvedValue({ id: 1, username: 'testuser' }),
+    updateUser: vi.fn().mockResolvedValue({ affected: 1 }),
+    deleteUser: vi.fn().mockResolvedValue({ affected: 1 }),
+    changePassword: vi.fn().mockResolvedValue({ affected: 1 }),
+    verifyPassword: vi.fn().mockResolvedValue(true),
+    assignRole: vi.fn().mockResolvedValue({ affected: 1 }),
+    removeRole: vi.fn().mockResolvedValue({ affected: 1 })
+  };
+}
+
+/**
+ * 创建模拟的认证服务
+ */
+export function createMockAuthService() {
+  return {
+    login: vi.fn().mockResolvedValue({
+      token: 'mock-jwt-token',
+      user: { id: 1, username: 'testuser' }
+    }),
+    logout: vi.fn().mockResolvedValue(undefined),
+    register: vi.fn().mockResolvedValue({ id: 1, username: 'newuser' }),
+    verifyToken: vi.fn().mockResolvedValue({ id: 1, username: 'testuser' }),
+    refreshToken: vi.fn().mockResolvedValue({ token: 'new-mock-jwt-token' }),
+    forgotPassword: vi.fn().mockResolvedValue({ success: true }),
+    resetPassword: vi.fn().mockResolvedValue({ success: true })
+  };
+}
+
+/**
+ * 创建模拟的角色服务
+ */
+export function createMockRoleService() {
+  return {
+    getRoles: vi.fn().mockResolvedValue([]),
+    getRoleById: vi.fn().mockResolvedValue(null),
+    createRole: vi.fn().mockResolvedValue({ id: 1, name: 'admin' }),
+    updateRole: vi.fn().mockResolvedValue({ affected: 1 }),
+    deleteRole: vi.fn().mockResolvedValue({ affected: 1 }),
+    assignPermission: vi.fn().mockResolvedValue({ affected: 1 }),
+    removePermission: vi.fn().mockResolvedValue({ affected: 1 })
+  };
+}
+
+/**
+ * 创建模拟的通用CRUD服务
+ */
+export function createMockCrudService() {
+  return {
+    findAll: vi.fn().mockResolvedValue([]),
+    findOne: vi.fn().mockResolvedValue(null),
+    create: vi.fn().mockResolvedValue({ id: 1 }),
+    update: vi.fn().mockResolvedValue({ affected: 1 }),
+    delete: vi.fn().mockResolvedValue({ affected: 1 }),
+    count: vi.fn().mockResolvedValue(0),
+    exists: vi.fn().mockResolvedValue(false)
+  };
+}
+
+/**
+ * 创建模拟的邮件服务
+ */
+export function createMockEmailService() {
+  return {
+    sendWelcomeEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendPasswordResetEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendNotification: vi.fn().mockResolvedValue({ success: true }),
+    verifyEmail: vi.fn().mockResolvedValue({ success: true })
+  };
+}
+
+/**
+ * 创建模拟的文件服务
+ */
+export function createMockFileService() {
+  return {
+    upload: vi.fn().mockResolvedValue({ url: 'https://example.com/file.jpg' }),
+    download: vi.fn().mockResolvedValue(Buffer.from('test content')),
+    delete: vi.fn().mockResolvedValue({ success: true }),
+    getSignedUrl: vi.fn().mockResolvedValue('https://example.com/signed-url')
+  };
+}
+
+/**
+ * 服务stub工具类
+ */
+export class ServiceStubManager {
+  private stubs = new Map<string, any>();
+
+  /**
+   * 创建服务stub
+   */
+  createStub<T>(serviceName: string, stubImplementation: Partial<T>): T {
+    const stub = { ...stubImplementation } as T;
+    this.stubs.set(serviceName, stub);
+    return stub;
+  }
+
+  /**
+   * 获取服务stub
+   */
+  getStub<T>(serviceName: string): T | undefined {
+    return this.stubs.get(serviceName);
+  }
+
+  /**
+   * 重置所有stub
+   */
+  resetAll(): void {
+    this.stubs.clear();
+  }
+
+  /**
+   * 重置特定stub
+   */
+  resetStub(serviceName: string): void {
+    this.stubs.delete(serviceName);
+  }
+}
+
+/**
+ * 全局服务stub管理器
+ */
+export const serviceStubs = new ServiceStubManager();
+
+/**
+ * 设置服务mock
+ */
+export function setupServiceMocks() {
+  // 用户服务mock
+  vi.mock('@d8d/server/modules/users/user.service', () => ({
+    UserService: vi.fn().mockImplementation(() => serviceStubs.createStub('UserService', createMockUserService()))
+  }));
+
+  // 认证服务mock
+  vi.mock('@d8d/server/modules/auth/auth.service', () => ({
+    AuthService: vi.fn().mockImplementation(() => serviceStubs.createStub('AuthService', createMockAuthService()))
+  }));
+
+  // 角色服务mock
+  vi.mock('@d8d/server/modules/roles/role.service', () => ({
+    RoleService: vi.fn().mockImplementation(() => serviceStubs.createStub('RoleService', createMockRoleService()))
+  }));
+
+  // 通用CRUD服务mock
+  vi.mock('@d8d/server/utils/generic-crud.service', () => ({
+    GenericCRUDService: vi.fn().mockImplementation(() => serviceStubs.createStub('GenericCRUDService', createMockCrudService()))
+  }));
+}

+ 88 - 0
packages/test-utils/src/server/test-auth.ts

@@ -0,0 +1,88 @@
+import { vi } from 'vitest';
+
+/**
+ * 创建模拟的认证上下文
+ */
+export function createMockAuthContext(overrides: Partial<any> = {}) {
+  const baseContext = {
+    req: {
+      header: (name: string) => {
+        const headers: Record<string, string> = {
+          'authorization': 'Bearer test-token-123',
+          'content-type': 'application/json',
+          'user-agent': 'vitest/integration-test',
+          'x-request-id': `test_${Math.random().toString(36).substr(2, 9)}`
+        };
+        return headers[name.toLowerCase()] || null;
+      }
+    },
+    set: vi.fn(),
+    json: vi.fn().mockImplementation((data, status = 200) => ({
+      status,
+      body: data
+    })),
+    status: vi.fn().mockReturnThis(),
+    body: vi.fn().mockReturnThis(),
+    env: {
+      NODE_ENV: 'test',
+      DATABASE_URL: process.env.TEST_DATABASE_URL || 'mysql://root:test@localhost:3306/test_d8dai'
+    },
+    var: {},
+    get: vi.fn()
+  };
+
+  return { ...baseContext, ...overrides };
+}
+
+/**
+ * 创建模拟的JWT用户信息
+ */
+export function createMockJwtPayload(overrides: Partial<any> = {}) {
+  return {
+    sub: '1',
+    username: 'testuser',
+    email: 'test@example.com',
+    roles: ['user'],
+    iat: Math.floor(Date.now() / 1000),
+    exp: Math.floor(Date.now() / 1000) + 3600, // 1小时后过期
+    ...overrides
+  };
+}
+
+/**
+ * 创建模拟的认证中间件
+ */
+export function createMockAuthMiddleware() {
+  return vi.fn().mockImplementation(async (c: any, next: () => Promise<void>) => {
+    // 模拟认证用户信息
+    c.set('user', {
+      id: 1,
+      username: 'testuser',
+      email: 'test@example.com',
+      roles: ['user']
+    });
+    await next();
+  });
+}
+
+/**
+ * 创建模拟的权限中间件
+ */
+export function createMockPermissionMiddleware(requiredRoles: string[] = []) {
+  return vi.fn().mockImplementation(async (c: any, next: () => Promise<void>) => {
+    const user = c.get('user');
+
+    if (!user) {
+      return c.json({ error: 'Unauthorized' }, 401);
+    }
+
+    if (requiredRoles.length > 0) {
+      const hasRole = requiredRoles.some(role => user.roles?.includes(role));
+      if (!hasRole) {
+        return c.json({ error: 'Forbidden' }, 403);
+      }
+    }
+
+    await next();
+  });
+}

+ 157 - 0
packages/test-utils/src/server/test-db.ts

@@ -0,0 +1,157 @@
+import { DataSource, EntityManager, Repository } from 'typeorm';
+import { vi, beforeEach, afterEach } from 'vitest';
+
+/**
+ * 创建模拟的数据源
+ */
+export function createMockDataSource() {
+  const manager = createMockEntityManager();
+  const mockDataSource = {
+    initialize: vi.fn().mockResolvedValue(undefined),
+    destroy: vi.fn().mockResolvedValue(undefined),
+    isInitialized: true,
+    manager,
+    getRepository: vi.fn().mockImplementation(() => createMockRepository()),
+    createQueryBuilder: vi.fn().mockReturnValue(createMockQueryBuilder()),
+    transaction: vi.fn().mockImplementation(async (callback) => {
+      return callback(manager);
+    }),
+    synchronize: vi.fn().mockResolvedValue(undefined),
+    dropDatabase: vi.fn().mockResolvedValue(undefined)
+  };
+
+  return mockDataSource;
+}
+
+/**
+ * 创建模拟的实体管理器
+ */
+export function createMockEntityManager(): EntityManager {
+  return {
+    find: vi.fn().mockResolvedValue([]),
+    findOne: vi.fn().mockResolvedValue(null),
+    save: vi.fn().mockImplementation((entity) => Promise.resolve(entity)),
+    update: vi.fn().mockResolvedValue({ affected: 1 }),
+    delete: vi.fn().mockResolvedValue({ affected: 1 }),
+    createQueryBuilder: vi.fn().mockReturnValue(createMockQueryBuilder()),
+    transaction: vi.fn().mockImplementation(async (callback) => {
+      return callback(createMockEntityManager());
+    }),
+    getRepository: vi.fn().mockImplementation(() => createMockRepository())
+  } as any;
+}
+
+/**
+ * 创建模拟的Repository
+ */
+export function createMockRepository<T extends object = any>(): Repository<T> {
+  return {
+    find: vi.fn().mockResolvedValue([]),
+    findOne: vi.fn().mockResolvedValue(null),
+    findOneBy: vi.fn().mockResolvedValue(null),
+    findOneByOrFail: vi.fn().mockResolvedValue(null),
+    findBy: vi.fn().mockResolvedValue([]),
+    findAndCount: vi.fn().mockResolvedValue([[], 0]),
+    findAndCountBy: vi.fn().mockResolvedValue([[], 0]),
+    save: vi.fn().mockImplementation((entity) => Promise.resolve(entity)),
+    update: vi.fn().mockResolvedValue({ affected: 1 }),
+    delete: vi.fn().mockResolvedValue({ affected: 1 }),
+    create: vi.fn().mockImplementation((entity) => ({ ...entity, id: Date.now() })),
+    createQueryBuilder: vi.fn().mockReturnValue(createMockQueryBuilder()),
+    count: vi.fn().mockResolvedValue(0),
+    countBy: vi.fn().mockResolvedValue(0),
+    exist: vi.fn().mockResolvedValue(false)
+  } as any;
+}
+
+/**
+ * 创建模拟的QueryBuilder
+ */
+export function createMockQueryBuilder() {
+  const mockQueryBuilder = {
+    select: vi.fn().mockReturnThis(),
+    from: vi.fn().mockReturnThis(),
+    where: vi.fn().mockReturnThis(),
+    andWhere: vi.fn().mockReturnThis(),
+    orWhere: vi.fn().mockReturnThis(),
+    leftJoin: vi.fn().mockReturnThis(),
+    innerJoin: vi.fn().mockReturnThis(),
+    orderBy: vi.fn().mockReturnThis(),
+    groupBy: vi.fn().mockReturnThis(),
+    having: vi.fn().mockReturnThis(),
+    skip: vi.fn().mockReturnThis(),
+    take: vi.fn().mockReturnThis(),
+    getMany: vi.fn().mockResolvedValue([]),
+    getOne: vi.fn().mockResolvedValue(null),
+    getCount: vi.fn().mockResolvedValue(0),
+    getRawMany: vi.fn().mockResolvedValue([]),
+    getRawOne: vi.fn().mockResolvedValue(null),
+    execute: vi.fn().mockResolvedValue(undefined),
+    setParameter: vi.fn().mockReturnThis(),
+    setParameters: vi.fn().mockReturnThis()
+  };
+
+  return mockQueryBuilder;
+}
+
+/**
+ * 数据库测试工具类
+ */
+export class TestDatabase {
+  private static dataSource: DataSource | null = null;
+
+  /**
+   * 初始化测试数据库
+   */
+  static async initialize(): Promise<DataSource> {
+    if (this.dataSource?.isInitialized) {
+      return this.dataSource;
+    }
+
+    // 使用SQLite内存数据库进行测试
+    this.dataSource = new DataSource({
+      type: 'better-sqlite3',
+      database: ':memory:',
+      synchronize: true,
+      logging: false,
+      entities: [
+        // 导入实际实体
+        (await import('@d8d/server/modules/users/user.entity')).UserEntity,
+        (await import('@d8d/server/modules/users/role.entity')).Role
+      ]
+    });
+
+    await this.dataSource.initialize();
+    return this.dataSource;
+  }
+
+  /**
+   * 清理测试数据库
+   */
+  static async cleanup(): Promise<void> {
+    if (this.dataSource?.isInitialized) {
+      await this.dataSource.destroy();
+      this.dataSource = null;
+    }
+  }
+
+  /**
+   * 获取当前数据源
+   */
+  static getDataSource(): DataSource | null {
+    return this.dataSource;
+  }
+}
+
+/**
+ * 测试数据库生命周期钩子
+ */
+export function setupDatabaseHooks() {
+  beforeEach(async () => {
+    await TestDatabase.initialize();
+  });
+
+  afterEach(async () => {
+    await TestDatabase.cleanup();
+  });
+}

+ 48 - 0
packages/test-utils/src/setup.ts

@@ -0,0 +1,48 @@
+// 测试环境全局设置
+import { beforeAll, afterAll, afterEach, vi, expect } from 'vitest';
+import * as matchers from '@testing-library/jest-dom/matchers';
+
+// 全局测试超时设置已在 vitest.config.ts 中配置
+
+// 扩展expect匹配器
+expect.extend(matchers);
+
+// 全局测试前置处理
+beforeAll(() => {
+  // 设置测试环境变量
+  process.env.NODE_ENV = 'test';
+
+  // 抑制控制台输出(测试中)
+  vi.spyOn(console, 'log').mockImplementation(() => {});
+  vi.spyOn(console, 'error').mockImplementation(() => {});
+  vi.spyOn(console, 'warn').mockImplementation(() => {});
+  vi.spyOn(console, 'info').mockImplementation(() => {});
+});
+
+// 全局测试后置清理
+afterAll(() => {
+  // 恢复控制台输出
+  vi.restoreAllMocks();
+});
+
+// 每个测试后的清理
+afterEach(() => {
+  vi.clearAllMocks();
+});
+
+// 全局测试工具函数
+globalThis.createTestContext = () => ({
+  timestamp: new Date().toISOString(),
+  requestId: `test_${Math.random().toString(36).substr(2, 9)}`
+});
+
+// 类型声明
+declare global {
+  // eslint-disable-next-line no-var
+  var createTestContext: () => {
+    timestamp: string;
+    requestId: string;
+  };
+}
+
+export {};

+ 90 - 0
packages/test-utils/src/test-utils.ts

@@ -0,0 +1,90 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { Hono } from 'hono';
+import { vi } from 'vitest';
+
+/**
+ * 创建测试服务器实例
+ */
+export function createTestServer(app: OpenAPIHono | Hono) {
+  const server = app as any;
+
+  return {
+    get: (path: string) => makeRequest('GET', path),
+    post: (path: string, body?: any) => makeRequest('POST', path, body),
+    put: (path: string, body?: any) => makeRequest('PUT', path, body),
+    delete: (path: string) => makeRequest('DELETE', path),
+    patch: (path: string, body?: any) => makeRequest('PATCH', path, body)
+  };
+
+  async function makeRequest(method: string, path: string, body?: any) {
+    const url = new URL(path, 'http://localhost:3000');
+
+    const request = new Request(url.toString(), {
+      method,
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': 'Bearer mock-token',
+      },
+      body: body ? JSON.stringify(body) : undefined,
+    });
+
+    try {
+      const response = await server.fetch(request);
+      return {
+        status: response.status,
+        headers: response.headers,
+        json: async () => response.json(),
+        text: async () => response.text()
+      };
+    } catch (error) {
+      throw new Error(`Request failed: ${error}`);
+    }
+  }
+}
+
+/**
+ * 创建模拟的认证上下文
+ */
+export function createMockAuthContext() {
+  return {
+    req: {
+      header: (name: string) => {
+        if (name === 'authorization') return 'Bearer mock-token';
+        return null;
+      }
+    },
+    set: vi.fn(),
+    json: vi.fn().mockImplementation((data, status = 200) => ({
+      status,
+      body: data
+    })),
+    env: {},
+    var: {}
+  };
+}
+
+/**
+ * 创建模拟的用户实体
+ */
+export function createMockUser(overrides: Partial<any> = {}) {
+  return {
+    id: 1,
+    username: 'testuser',
+    email: 'test@example.com',
+    password: 'hashed_password',
+    phone: '13800138000',
+    nickname: 'Test User',
+    status: 1,
+    createdAt: new Date(),
+    updatedAt: new Date(),
+    roles: [],
+    ...overrides
+  };
+}
+
+/**
+ * 等待指定时间
+ */
+export function wait(ms: number) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}

+ 36 - 0
packages/test-utils/tsconfig.json

@@ -0,0 +1,36 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": false,
+    "jsx": "react-jsx",
+    "strict": true,
+    "noUncheckedIndexedAccess": true,
+    "noImplicitOverride": true,
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "declaration": true,
+    "skipLibCheck": true,
+
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": [
+    "src/**/*",
+    "tests/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}

+ 371 - 24
pnpm-lock.yaml

@@ -281,6 +281,40 @@ importers:
         specifier: ^5.8.3
         version: 5.8.3
 
+  packages/test-utils:
+    dependencies:
+      '@d8d/server':
+        specifier: workspace:*
+        version: link:../server
+      '@testing-library/jest-dom':
+        specifier: ^6.4.2
+        version: 6.9.1
+      '@testing-library/react':
+        specifier: ^13.4.0
+        version: 13.4.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@testing-library/user-event':
+        specifier: ^14.5.2
+        version: 14.6.1(@testing-library/dom@10.4.1)
+      '@types/node':
+        specifier: ^20.10.5
+        version: 20.19.23
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      react:
+        specifier: ^19.1.0
+        version: 19.2.0
+      react-dom:
+        specifier: ^19.1.0
+        version: 19.2.0(react@19.2.0)
+    devDependencies:
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      vitest:
+        specifier: ^2.0.5
+        version: 2.1.9(@types/node@20.19.23)(happy-dom@18.0.1)(jsdom@24.1.3)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)
+
   web:
     dependencies:
       '@ant-design/icons':
@@ -3730,10 +3764,21 @@ packages:
     resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
     engines: {node: '>=18'}
 
+  '@testing-library/dom@8.20.1':
+    resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==}
+    engines: {node: '>=12'}
+
   '@testing-library/jest-dom@6.9.1':
     resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
     engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
 
+  '@testing-library/react@13.4.0':
+    resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==}
+    engines: {node: '>=12'}
+    peerDependencies:
+      react: ^18.0.0
+      react-dom: ^18.0.0
+
   '@testing-library/react@16.3.0':
     resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==}
     engines: {node: '>=18'}
@@ -3922,6 +3967,11 @@ packages:
   '@types/range-parser@1.2.7':
     resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
 
+  '@types/react-dom@18.3.7':
+    resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
+    peerDependencies:
+      '@types/react': ^18.0.0
+
   '@types/react-dom@19.2.2':
     resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==}
     peerDependencies:
@@ -4113,9 +4163,23 @@ packages:
       '@vitest/browser':
         optional: true
 
+  '@vitest/expect@2.1.9':
+    resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
+
   '@vitest/expect@3.2.4':
     resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
 
+  '@vitest/mocker@2.1.9':
+    resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
+    peerDependencies:
+      msw: ^2.4.9
+      vite: ^5.0.0
+    peerDependenciesMeta:
+      msw:
+        optional: true
+      vite:
+        optional: true
+
   '@vitest/mocker@3.2.4':
     resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
     peerDependencies:
@@ -4127,18 +4191,33 @@ packages:
       vite:
         optional: true
 
+  '@vitest/pretty-format@2.1.9':
+    resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
+
   '@vitest/pretty-format@3.2.4':
     resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
 
+  '@vitest/runner@2.1.9':
+    resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
+
   '@vitest/runner@3.2.4':
     resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
 
+  '@vitest/snapshot@2.1.9':
+    resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
+
   '@vitest/snapshot@3.2.4':
     resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
 
+  '@vitest/spy@2.1.9':
+    resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
+
   '@vitest/spy@3.2.4':
     resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
 
+  '@vitest/utils@2.1.9':
+    resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
+
   '@vitest/utils@3.2.4':
     resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
 
@@ -4390,6 +4469,9 @@ packages:
     resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
     engines: {node: '>=10'}
 
+  aria-query@5.1.3:
+    resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
+
   aria-query@5.3.0:
     resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
 
@@ -5285,6 +5367,10 @@ packages:
     resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
     engines: {node: '>=6'}
 
+  deep-equal@2.2.3:
+    resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
+    engines: {node: '>= 0.4'}
+
   deep-extend@0.6.0:
     resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
     engines: {node: '>=4.0.0'}
@@ -5554,6 +5640,9 @@ packages:
     resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
     engines: {node: '>= 0.4'}
 
+  es-get-iterator@1.1.3:
+    resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
+
   es-iterator-helpers@1.2.1:
     resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==}
     engines: {node: '>= 0.4'}
@@ -7439,6 +7528,10 @@ packages:
     resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
     engines: {node: '>= 0.4'}
 
+  object-is@1.1.6:
+    resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
+    engines: {node: '>= 0.4'}
+
   object-keys@1.1.1:
     resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
     engines: {node: '>= 0.4'}
@@ -7653,6 +7746,9 @@ packages:
     resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==}
     engines: {node: '>=18'}
 
+  pathe@1.1.2:
+    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
   pathe@2.0.3:
     resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
 
@@ -9263,10 +9359,18 @@ packages:
     resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
     engines: {node: ^18.0.0 || >=20.0.0}
 
+  tinyrainbow@1.2.0:
+    resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+    engines: {node: '>=14.0.0'}
+
   tinyrainbow@2.0.0:
     resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
     engines: {node: '>=14.0.0'}
 
+  tinyspy@3.0.2:
+    resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+    engines: {node: '>=14.0.0'}
+
   tinyspy@4.0.4:
     resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
     engines: {node: '>=14.0.0'}
@@ -9624,6 +9728,11 @@ packages:
   victory-vendor@36.9.2:
     resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
 
+  vite-node@2.1.9:
+    resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+
   vite-node@3.2.4:
     resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -9639,6 +9748,37 @@ packages:
     peerDependencies:
       vite: ^3.0.0 || ^4.0.0 || ^5.0.0
 
+  vite@5.4.21:
+    resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || >=20.0.0
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      sass-embedded: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+
   vite@7.1.11:
     resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==}
     engines: {node: ^20.19.0 || >=22.12.0}
@@ -9679,6 +9819,31 @@ packages:
       yaml:
         optional: true
 
+  vitest@2.1.9:
+    resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@edge-runtime/vm': '*'
+      '@types/node': ^18.0.0 || >=20.0.0
+      '@vitest/browser': 2.1.9
+      '@vitest/ui': 2.1.9
+      happy-dom: '*'
+      jsdom: '*'
+    peerDependenciesMeta:
+      '@edge-runtime/vm':
+        optional: true
+      '@types/node':
+        optional: true
+      '@vitest/browser':
+        optional: true
+      '@vitest/ui':
+        optional: true
+      happy-dom:
+        optional: true
+      jsdom:
+        optional: true
+
   vitest@3.2.4:
     resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -11660,7 +11825,7 @@ snapshots:
     dependencies:
       '@types/istanbul-lib-coverage': 2.0.6
       '@types/istanbul-reports': 3.0.4
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       '@types/yargs': 15.0.19
       chalk: 4.1.2
 
@@ -11669,7 +11834,7 @@ snapshots:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.6
       '@types/istanbul-reports': 3.0.4
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       '@types/yargs': 17.0.33
       chalk: 4.1.2
 
@@ -13317,6 +13482,17 @@ snapshots:
       picocolors: 1.1.1
       pretty-format: 27.5.1
 
+  '@testing-library/dom@8.20.1':
+    dependencies:
+      '@babel/code-frame': 7.27.1
+      '@babel/runtime': 7.28.4
+      '@types/aria-query': 5.0.4
+      aria-query: 5.1.3
+      chalk: 4.1.2
+      dom-accessibility-api: 0.5.16
+      lz-string: 1.5.0
+      pretty-format: 27.5.1
+
   '@testing-library/jest-dom@6.9.1':
     dependencies:
       '@adobe/css-tools': 4.4.4
@@ -13326,6 +13502,16 @@ snapshots:
       picocolors: 1.1.1
       redent: 3.0.0
 
+  '@testing-library/react@13.4.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@babel/runtime': 7.28.4
+      '@testing-library/dom': 8.20.1
+      '@types/react-dom': 18.3.7(@types/react@19.2.2)
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - '@types/react'
+
   '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
     dependencies:
       '@babel/runtime': 7.28.4
@@ -13359,11 +13545,11 @@ snapshots:
   '@types/body-parser@1.19.6':
     dependencies:
       '@types/connect': 3.4.38
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/bonjour@3.5.13':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/chai@5.2.3':
     dependencies:
@@ -13373,15 +13559,15 @@ snapshots:
   '@types/connect-history-api-fallback@1.5.4':
     dependencies:
       '@types/express-serve-static-core': 5.1.0
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/connect@3.4.38':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/conventional-commits-parser@5.0.1':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/d3-array@3.2.2': {}
 
@@ -13427,14 +13613,14 @@ snapshots:
 
   '@types/express-serve-static-core@4.19.7':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       '@types/qs': 6.14.0
       '@types/range-parser': 1.2.7
       '@types/send': 1.2.0
 
   '@types/express-serve-static-core@5.1.0':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       '@types/qs': 6.14.0
       '@types/range-parser': 1.2.7
       '@types/send': 1.2.0
@@ -13448,12 +13634,12 @@ snapshots:
 
   '@types/fs-extra@8.1.5':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/glob@7.2.0':
     dependencies:
       '@types/minimatch': 6.0.0
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/html-minifier-terser@6.1.0': {}
 
@@ -13461,7 +13647,7 @@ snapshots:
 
   '@types/http-proxy@1.17.16':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/istanbul-lib-coverage@2.0.6': {}
 
@@ -13484,7 +13670,7 @@ snapshots:
 
   '@types/keyv@3.1.4':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/lodash.debounce@4.0.9':
     dependencies:
@@ -13504,7 +13690,7 @@ snapshots:
 
   '@types/node-forge@1.3.14':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/node@18.19.130':
     dependencies:
@@ -13526,7 +13712,7 @@ snapshots:
 
   '@types/postcss-url@10.0.4':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       postcss: 8.5.6
 
   '@types/prop-types@15.7.15': {}
@@ -13535,6 +13721,10 @@ snapshots:
 
   '@types/range-parser@1.2.7': {}
 
+  '@types/react-dom@18.3.7(@types/react@19.2.2)':
+    dependencies:
+      '@types/react': 19.2.2
+
   '@types/react-dom@19.2.2(@types/react@19.2.2)':
     dependencies:
       '@types/react': 19.2.2
@@ -13550,7 +13740,7 @@ snapshots:
 
   '@types/responselike@1.0.3':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/retry@0.12.0': {}
 
@@ -13563,11 +13753,11 @@ snapshots:
   '@types/send@0.17.5':
     dependencies:
       '@types/mime': 1.3.5
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/send@1.2.0':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/serve-index@1.9.4':
     dependencies:
@@ -13576,12 +13766,12 @@ snapshots:
   '@types/serve-static@1.15.9':
     dependencies:
       '@types/http-errors': 2.0.5
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       '@types/send': 0.17.5
 
   '@types/sockjs@0.3.36':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/webpack-env@1.18.8': {}
 
@@ -13589,7 +13779,7 @@ snapshots:
 
   '@types/ws@8.18.1':
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
 
   '@types/yargs-parser@21.0.3': {}
 
@@ -13809,6 +13999,13 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@vitest/expect@2.1.9':
+    dependencies:
+      '@vitest/spy': 2.1.9
+      '@vitest/utils': 2.1.9
+      chai: 5.3.3
+      tinyrainbow: 1.2.0
+
   '@vitest/expect@3.2.4':
     dependencies:
       '@types/chai': 5.2.3
@@ -13817,6 +14014,14 @@ snapshots:
       chai: 5.3.3
       tinyrainbow: 2.0.0
 
+  '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0))':
+    dependencies:
+      '@vitest/spy': 2.1.9
+      estree-walker: 3.0.3
+      magic-string: 0.30.19
+    optionalDependencies:
+      vite: 5.4.21(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)
+
   '@vitest/mocker@3.2.4(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))':
     dependencies:
       '@vitest/spy': 3.2.4
@@ -13825,26 +14030,51 @@ snapshots:
     optionalDependencies:
       vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
 
+  '@vitest/pretty-format@2.1.9':
+    dependencies:
+      tinyrainbow: 1.2.0
+
   '@vitest/pretty-format@3.2.4':
     dependencies:
       tinyrainbow: 2.0.0
 
+  '@vitest/runner@2.1.9':
+    dependencies:
+      '@vitest/utils': 2.1.9
+      pathe: 1.1.2
+
   '@vitest/runner@3.2.4':
     dependencies:
       '@vitest/utils': 3.2.4
       pathe: 2.0.3
       strip-literal: 3.1.0
 
+  '@vitest/snapshot@2.1.9':
+    dependencies:
+      '@vitest/pretty-format': 2.1.9
+      magic-string: 0.30.19
+      pathe: 1.1.2
+
   '@vitest/snapshot@3.2.4':
     dependencies:
       '@vitest/pretty-format': 3.2.4
       magic-string: 0.30.19
       pathe: 2.0.3
 
+  '@vitest/spy@2.1.9':
+    dependencies:
+      tinyspy: 3.0.2
+
   '@vitest/spy@3.2.4':
     dependencies:
       tinyspy: 4.0.4
 
+  '@vitest/utils@2.1.9':
+    dependencies:
+      '@vitest/pretty-format': 2.1.9
+      loupe: 3.2.1
+      tinyrainbow: 1.2.0
+
   '@vitest/utils@3.2.4':
     dependencies:
       '@vitest/pretty-format': 3.2.4
@@ -14148,6 +14378,10 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  aria-query@5.1.3:
+    dependencies:
+      deep-equal: 2.2.3
+
   aria-query@5.3.0:
     dependencies:
       dequal: 2.0.3
@@ -15172,6 +15406,27 @@ snapshots:
 
   deep-eql@5.0.2: {}
 
+  deep-equal@2.2.3:
+    dependencies:
+      array-buffer-byte-length: 1.0.2
+      call-bind: 1.0.8
+      es-get-iterator: 1.1.3
+      get-intrinsic: 1.3.0
+      is-arguments: 1.2.0
+      is-array-buffer: 3.0.5
+      is-date-object: 1.1.0
+      is-regex: 1.2.1
+      is-shared-array-buffer: 1.0.4
+      isarray: 2.0.5
+      object-is: 1.1.6
+      object-keys: 1.1.1
+      object.assign: 4.1.7
+      regexp.prototype.flags: 1.5.4
+      side-channel: 1.1.0
+      which-boxed-primitive: 1.1.1
+      which-collection: 1.0.2
+      which-typed-array: 1.1.19
+
   deep-extend@0.6.0: {}
 
   deep-is@0.1.4: {}
@@ -15476,6 +15731,18 @@ snapshots:
 
   es-errors@1.3.0: {}
 
+  es-get-iterator@1.1.3:
+    dependencies:
+      call-bind: 1.0.8
+      get-intrinsic: 1.3.0
+      has-symbols: 1.1.0
+      is-arguments: 1.2.0
+      is-map: 2.0.3
+      is-set: 2.0.3
+      is-string: 1.1.1
+      isarray: 2.0.5
+      stop-iteration-iterator: 1.1.0
+
   es-iterator-helpers@1.2.1:
     dependencies:
       call-bind: 1.0.8
@@ -16925,7 +17192,7 @@ snapshots:
   jest-util@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       chalk: 4.1.2
       ci-info: 3.9.0
       graceful-fs: 4.2.11
@@ -16933,13 +17200,13 @@ snapshots:
 
   jest-worker@27.5.1:
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
   jest-worker@29.7.0:
     dependencies:
-      '@types/node': 18.19.130
+      '@types/node': 24.9.1
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
@@ -17600,6 +17867,11 @@ snapshots:
 
   object-inspect@1.13.4: {}
 
+  object-is@1.1.6:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+
   object-keys@1.1.1: {}
 
   object.assign@4.1.7:
@@ -17826,6 +18098,8 @@ snapshots:
 
   path-type@6.0.0: {}
 
+  pathe@1.1.2: {}
+
   pathe@2.0.3: {}
 
   pathval@2.0.1: {}
@@ -19611,8 +19885,12 @@ snapshots:
 
   tinypool@1.1.1: {}
 
+  tinyrainbow@1.2.0: {}
+
   tinyrainbow@2.0.0: {}
 
+  tinyspy@3.0.2: {}
+
   tinyspy@4.0.4: {}
 
   to-buffer@1.2.2:
@@ -19940,6 +20218,24 @@ snapshots:
       d3-time: 3.1.0
       d3-timer: 3.0.1
 
+  vite-node@2.1.9(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0):
+    dependencies:
+      cac: 6.7.14
+      debug: 4.4.3
+      es-module-lexer: 1.7.0
+      pathe: 1.1.2
+      vite: 5.4.21(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)
+    transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
   vite-node@3.2.4(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1):
     dependencies:
       cac: 6.7.14
@@ -19970,6 +20266,20 @@ snapshots:
     dependencies:
       vite: 7.1.11(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
 
+  vite@5.4.21(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0):
+    dependencies:
+      esbuild: 0.21.5
+      postcss: 8.5.6
+      rollup: 4.52.5
+    optionalDependencies:
+      '@types/node': 20.19.23
+      fsevents: 2.3.3
+      less: 3.13.1
+      lightningcss: 1.30.2
+      sass: 1.93.2
+      stylus: 0.64.0
+      terser: 5.44.0
+
   vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1):
     dependencies:
       esbuild: 0.25.11
@@ -19989,6 +20299,43 @@ snapshots:
       tsx: 4.20.6
       yaml: 2.8.1
 
+  vitest@2.1.9(@types/node@20.19.23)(happy-dom@18.0.1)(jsdom@24.1.3)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0):
+    dependencies:
+      '@vitest/expect': 2.1.9
+      '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0))
+      '@vitest/pretty-format': 2.1.9
+      '@vitest/runner': 2.1.9
+      '@vitest/snapshot': 2.1.9
+      '@vitest/spy': 2.1.9
+      '@vitest/utils': 2.1.9
+      chai: 5.3.3
+      debug: 4.4.3
+      expect-type: 1.2.2
+      magic-string: 0.30.19
+      pathe: 1.1.2
+      std-env: 3.10.0
+      tinybench: 2.9.0
+      tinyexec: 0.3.2
+      tinypool: 1.1.1
+      tinyrainbow: 1.2.0
+      vite: 5.4.21(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)
+      vite-node: 2.1.9(@types/node@20.19.23)(less@3.13.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)
+      why-is-node-running: 2.3.0
+    optionalDependencies:
+      '@types/node': 20.19.23
+      happy-dom: 18.0.1
+      jsdom: 24.1.3
+    transitivePeerDependencies:
+      - less
+      - lightningcss
+      - msw
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
   vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.9.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1):
     dependencies:
       '@types/chai': 5.2.3