test: migrate to MSW testing architecture for App integration test
Major testing infrastructure upgrade from manual mocks to Mock Service Worker (MSW): New MSW infrastructure (tests/msw/): - Add state.ts: In-memory state manager with full CRUD operations - Manage providers and current selections per app type (Claude/Codex) - Auto-switch current provider when deleted - Deep clone to prevent reference pollution - Add handlers.ts: HTTP request handlers for 10 Tauri API endpoints - Mock get_providers, switch_provider, add/update/delete_provider - Mock update_sort_order, update_tray_menu, import_default_config - Support error scenarios (404 for non-existent providers) - Add tauriMocks.ts: Tauri API mock layer - Transparently convert invoke() calls to HTTP requests - Mock event listener system with emitTauriEvent helper - Use cross-fetch for Node.js environment - Add server.ts: MSW server setup for Node.js test environment Refactor App.test.tsx (-170 lines, -43%): - Remove 23 manual mocks (useProvidersQuery, useProviderActions, etc.) - Run real hooks with MSW-backed API calls instead of mock implementations - Test real state changes instead of mock call counts - Add comprehensive flow: duplicate → create → edit → delete → event listening - Verify actual provider list changes and current selection updates Setup integration: - Add MSW server lifecycle to tests/setupTests.ts - Start server before all tests - Reset handlers and state after each test - Close server after all tests complete - Clear all mocks in afterEach for test isolation Dependencies: - Add msw@^2.11.6 for API mocking - Add cross-fetch@^4.1.0 for fetch polyfill in Node.js Type fixes: - Add missing imports (AppType, Provider) in handlers.ts - Fix HttpResponse.json generic constraint with as any (MSW best practice) - Change invalid category "default" to "official" in state.ts Test results: All 50 tests passing across 8 files, 0 TypeScript errors
This commit is contained in:
@@ -19,14 +19,16 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.8.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"msw": "^2.11.6",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
|
||||
411
pnpm-lock.yaml
generated
411
pnpm-lock.yaml
generated
@@ -153,9 +153,15 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.2.0
|
||||
version: 4.7.0(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))
|
||||
cross-fetch:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
jsdom:
|
||||
specifier: ^25.0.0
|
||||
version: 25.0.1
|
||||
msw:
|
||||
specifier: ^2.11.6
|
||||
version: 2.11.6(@types/node@20.19.9)(typescript@5.9.2)
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
@@ -167,7 +173,7 @@ importers:
|
||||
version: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)
|
||||
vitest:
|
||||
specifier: ^2.0.5
|
||||
version: 2.1.9(@types/node@20.19.9)(jsdom@25.0.1)(lightningcss@1.30.1)
|
||||
version: 2.1.9(@types/node@20.19.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.11.6(@types/node@20.19.9)(typescript@5.9.2))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -506,6 +512,41 @@ packages:
|
||||
peerDependencies:
|
||||
react-hook-form: ^7.55.0
|
||||
|
||||
'@inquirer/ansi@1.0.1':
|
||||
resolution: {integrity: sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@inquirer/confirm@5.1.19':
|
||||
resolution: {integrity: sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.3.0':
|
||||
resolution: {integrity: sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@1.0.14':
|
||||
resolution: {integrity: sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@inquirer/type@3.0.9':
|
||||
resolution: {integrity: sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -547,6 +588,19 @@ packages:
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@mswjs/interceptors@0.40.0':
|
||||
resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@open-draft/deferred-promise@2.2.0':
|
||||
resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
|
||||
|
||||
'@open-draft/logger@0.3.0':
|
||||
resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
|
||||
|
||||
'@open-draft/until@2.1.0':
|
||||
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
|
||||
|
||||
'@radix-ui/number@1.1.1':
|
||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||
|
||||
@@ -1287,6 +1341,9 @@ packages:
|
||||
'@types/react@18.3.23':
|
||||
resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==}
|
||||
|
||||
'@types/statuses@2.0.6':
|
||||
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0':
|
||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -1330,6 +1387,10 @@ packages:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1383,6 +1444,14 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
cli-width@4.1.0:
|
||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1390,6 +1459,13 @@ packages:
|
||||
codemirror@6.0.2:
|
||||
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1397,9 +1473,16 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie@1.0.2:
|
||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
cross-fetch@4.1.0:
|
||||
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
|
||||
|
||||
css.escape@1.5.1:
|
||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||
|
||||
@@ -1458,6 +1541,9 @@ packages:
|
||||
electron-to-chromium@1.5.197:
|
||||
resolution: {integrity: sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
enhanced-resolve@5.18.3:
|
||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -1517,6 +1603,10 @@ packages:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1536,6 +1626,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
graphql@16.11.0:
|
||||
resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1548,6 +1642,9 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
headers-polyfill@4.0.3:
|
||||
resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1579,6 +1676,13 @@ packages:
|
||||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-node-process@1.2.0:
|
||||
resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
@@ -1736,20 +1840,49 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
msw@2.11.6:
|
||||
resolution: {integrity: sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>= 4.8.x'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
mute-stream@2.0.0:
|
||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
nwsapi@2.2.22:
|
||||
resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==}
|
||||
|
||||
outvariant@1.4.3:
|
||||
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
|
||||
pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
|
||||
@@ -1849,6 +1982,13 @@ packages:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
rettime@0.7.0:
|
||||
resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==}
|
||||
|
||||
rollup@4.46.2:
|
||||
resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -1877,6 +2017,10 @@ packages:
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
smol-toml@1.4.2:
|
||||
resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -1894,9 +2038,24 @@ packages:
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
strict-event-emitter@0.5.1:
|
||||
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-indent@3.0.0:
|
||||
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1942,14 +2101,28 @@ packages:
|
||||
tldts-core@6.1.86:
|
||||
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
|
||||
|
||||
tldts-core@7.0.17:
|
||||
resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==}
|
||||
|
||||
tldts@6.1.86:
|
||||
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
||||
hasBin: true
|
||||
|
||||
tldts@7.0.17:
|
||||
resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==}
|
||||
hasBin: true
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
tr46@5.1.1:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1957,6 +2130,10 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typescript@5.9.2:
|
||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -1965,6 +2142,9 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
until-async@3.0.2:
|
||||
resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
|
||||
|
||||
update-browserslist-db@1.1.3:
|
||||
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
|
||||
hasBin: true
|
||||
@@ -2063,6 +2243,9 @@ packages:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2079,11 +2262,22 @@ packages:
|
||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ws@8.18.3:
|
||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -2103,6 +2297,10 @@ packages:
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@@ -2110,6 +2308,18 @@ packages:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@17.7.2:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yoctocolors-cjs@2.1.3:
|
||||
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zod@4.1.12:
|
||||
resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
|
||||
|
||||
@@ -2448,6 +2658,34 @@ snapshots:
|
||||
'@standard-schema/utils': 0.3.0
|
||||
react-hook-form: 7.65.0(react@18.3.1)
|
||||
|
||||
'@inquirer/ansi@1.0.1': {}
|
||||
|
||||
'@inquirer/confirm@5.1.19(@types/node@20.19.9)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.3.0(@types/node@20.19.9)
|
||||
'@inquirer/type': 3.0.9(@types/node@20.19.9)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.9
|
||||
|
||||
'@inquirer/core@10.3.0(@types/node@20.19.9)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 1.0.1
|
||||
'@inquirer/figures': 1.0.14
|
||||
'@inquirer/type': 3.0.9(@types/node@20.19.9)
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.9
|
||||
|
||||
'@inquirer/figures@1.0.14': {}
|
||||
|
||||
'@inquirer/type@3.0.9(@types/node@20.19.9)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.9
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
@@ -2497,6 +2735,24 @@ snapshots:
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@mswjs/interceptors@0.40.0':
|
||||
dependencies:
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@open-draft/logger': 0.3.0
|
||||
'@open-draft/until': 2.1.0
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
strict-event-emitter: 0.5.1
|
||||
|
||||
'@open-draft/deferred-promise@2.2.0': {}
|
||||
|
||||
'@open-draft/logger@0.3.0':
|
||||
dependencies:
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
|
||||
'@open-draft/until@2.1.0': {}
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
@@ -3142,6 +3398,8 @@ snapshots:
|
||||
'@types/prop-types': 15.7.15
|
||||
csstype: 3.1.3
|
||||
|
||||
'@types/statuses@2.0.6': {}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
@@ -3161,12 +3419,13 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/mocker@2.1.9(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))':
|
||||
'@vitest/mocker@2.1.9(msw@2.11.6(@types/node@20.19.9)(typescript@5.9.2))(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.18
|
||||
optionalDependencies:
|
||||
msw: 2.11.6(@types/node@20.19.9)(typescript@5.9.2)
|
||||
vite: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
@@ -3198,6 +3457,10 @@ snapshots:
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
@@ -3246,6 +3509,14 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
cli-width@4.1.0: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
codemirror@6.0.2:
|
||||
@@ -3258,14 +3529,28 @@ snapshots:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.2
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie@1.0.2: {}
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cross-fetch@4.1.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
css.escape@1.5.1: {}
|
||||
|
||||
cssstyle@4.6.0:
|
||||
@@ -3308,6 +3593,8 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.197: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
enhanced-resolve@5.18.3:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -3381,6 +3668,8 @@ snapshots:
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -3405,6 +3694,8 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
graphql@16.11.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
@@ -3415,6 +3706,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
headers-polyfill@4.0.3: {}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
@@ -3449,6 +3742,10 @@ snapshots:
|
||||
|
||||
indent-string@4.0.0: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-node-process@1.2.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
||||
jiti@2.5.1: {}
|
||||
@@ -3576,16 +3873,51 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msw@2.11.6(@types/node@20.19.9)(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@inquirer/confirm': 5.1.19(@types/node@20.19.9)
|
||||
'@mswjs/interceptors': 0.40.0
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@types/statuses': 2.0.6
|
||||
cookie: 1.0.2
|
||||
graphql: 16.11.0
|
||||
headers-polyfill: 4.0.3
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
path-to-regexp: 6.3.0
|
||||
picocolors: 1.1.1
|
||||
rettime: 0.7.0
|
||||
statuses: 2.0.2
|
||||
strict-event-emitter: 0.5.1
|
||||
tough-cookie: 6.0.0
|
||||
type-fest: 4.41.0
|
||||
until-async: 3.0.2
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
nwsapi@2.2.22: {}
|
||||
|
||||
outvariant@1.4.3: {}
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
pathe@1.1.2: {}
|
||||
|
||||
pathval@2.0.1: {}
|
||||
@@ -3668,6 +4000,10 @@ snapshots:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
rettime@0.7.0: {}
|
||||
|
||||
rollup@4.46.2:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -3712,6 +4048,8 @@ snapshots:
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
smol-toml@1.4.2: {}
|
||||
|
||||
sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
@@ -3723,8 +4061,22 @@ snapshots:
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
strict-event-emitter@0.5.1: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-indent@3.0.0:
|
||||
dependencies:
|
||||
min-indent: 1.0.1
|
||||
@@ -3760,24 +4112,40 @@ snapshots:
|
||||
|
||||
tldts-core@6.1.86: {}
|
||||
|
||||
tldts-core@7.0.17: {}
|
||||
|
||||
tldts@6.1.86:
|
||||
dependencies:
|
||||
tldts-core: 6.1.86
|
||||
|
||||
tldts@7.0.17:
|
||||
dependencies:
|
||||
tldts-core: 7.0.17
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
dependencies:
|
||||
tldts: 6.1.86
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
dependencies:
|
||||
tldts: 7.0.17
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tr46@5.1.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typescript@5.9.2: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
until-async@3.0.2: {}
|
||||
|
||||
update-browserslist-db@1.1.3(browserslist@4.25.1):
|
||||
dependencies:
|
||||
browserslist: 4.25.1
|
||||
@@ -3827,10 +4195,10 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
lightningcss: 1.30.1
|
||||
|
||||
vitest@2.1.9(@types/node@20.19.9)(jsdom@25.0.1)(lightningcss@1.30.1):
|
||||
vitest@2.1.9(@types/node@20.19.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.11.6(@types/node@20.19.9)(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))
|
||||
'@vitest/mocker': 2.1.9(msw@2.11.6(@types/node@20.19.9)(typescript@5.9.2))(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1))
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
'@vitest/runner': 2.1.9
|
||||
'@vitest/snapshot': 2.1.9
|
||||
@@ -3871,6 +4239,8 @@ snapshots:
|
||||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
@@ -3884,19 +4254,52 @@ snapshots:
|
||||
tr46: 5.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
ws@8.18.3: {}
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
dependencies:
|
||||
cliui: 8.0.1
|
||||
escalade: 3.2.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yoctocolors-cjs@2.1.3: {}
|
||||
|
||||
zod@4.1.12: {}
|
||||
|
||||
@@ -1,144 +1,21 @@
|
||||
import { Suspense } from "react";
|
||||
import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import App from "@/App";
|
||||
import { resetProviderState, listProviders } from "../msw/state";
|
||||
import { emitTauriEvent } from "../msw/tauriMocks";
|
||||
|
||||
const {
|
||||
toastSuccessMock,
|
||||
toastErrorMock,
|
||||
deleteProviderMock,
|
||||
addProviderMock,
|
||||
updateProviderMock,
|
||||
saveUsageScriptMock,
|
||||
onSwitchedMock,
|
||||
updateSortOrderMock,
|
||||
updateTrayMenuMock,
|
||||
openExternalMock,
|
||||
useProvidersQueryMock,
|
||||
useProviderActionsMock,
|
||||
providersDataMock,
|
||||
getAllMock,
|
||||
getCurrentMock,
|
||||
importDefaultMock,
|
||||
} = vi.hoisted(() => {
|
||||
const deleteProviderMock = vi.fn();
|
||||
const addProviderMock = vi.fn();
|
||||
const updateProviderMock = vi.fn();
|
||||
const saveUsageScriptMock = vi.fn();
|
||||
const onSwitchedMock = vi.fn();
|
||||
const updateSortOrderMock = vi.fn();
|
||||
const updateTrayMenuMock = vi.fn();
|
||||
const openExternalMock = vi.fn();
|
||||
const useProvidersQueryMock = vi.fn();
|
||||
const getAllMock = vi.fn();
|
||||
const getCurrentMock = vi.fn();
|
||||
const importDefaultMock = vi.fn();
|
||||
const toastSuccessMock = vi.fn();
|
||||
const toastErrorMock = vi.fn();
|
||||
|
||||
const providersDataMock = {
|
||||
claude: [
|
||||
{
|
||||
id: "claude-1",
|
||||
name: "Claude Default",
|
||||
settingsConfig: {},
|
||||
category: "default",
|
||||
sortIndex: 0,
|
||||
},
|
||||
{
|
||||
id: "claude-2",
|
||||
name: "Claude Custom",
|
||||
settingsConfig: {},
|
||||
category: "custom",
|
||||
sortIndex: 1,
|
||||
},
|
||||
],
|
||||
codex: [
|
||||
{
|
||||
id: "codex-1",
|
||||
name: "Codex Default",
|
||||
settingsConfig: {},
|
||||
category: "default",
|
||||
sortIndex: 0,
|
||||
},
|
||||
{
|
||||
id: "codex-2",
|
||||
name: "Codex Secondary",
|
||||
settingsConfig: {},
|
||||
category: "custom",
|
||||
sortIndex: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useProviderActionsMock = vi.fn(() => ({
|
||||
addProvider: addProviderMock,
|
||||
updateProvider: updateProviderMock,
|
||||
deleteProvider: deleteProviderMock,
|
||||
saveUsageScript: saveUsageScriptMock,
|
||||
}));
|
||||
|
||||
return {
|
||||
toastSuccessMock,
|
||||
toastErrorMock,
|
||||
deleteProviderMock,
|
||||
addProviderMock,
|
||||
updateProviderMock,
|
||||
saveUsageScriptMock,
|
||||
onSwitchedMock,
|
||||
updateSortOrderMock,
|
||||
updateTrayMenuMock,
|
||||
openExternalMock,
|
||||
useProvidersQueryMock,
|
||||
useProviderActionsMock,
|
||||
providersDataMock,
|
||||
getAllMock,
|
||||
getCurrentMock,
|
||||
importDefaultMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/query", () => ({
|
||||
useProvidersQuery: (...args: unknown[]) => useProvidersQueryMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useProviderActions", () => ({
|
||||
useProviderActions: () => useProviderActionsMock(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
providersApi: {
|
||||
onSwitched: onSwitchedMock,
|
||||
updateSortOrder: updateSortOrderMock,
|
||||
updateTrayMenu: updateTrayMenuMock,
|
||||
getAll: getAllMock,
|
||||
getCurrent: getCurrentMock,
|
||||
importDefault: importDefaultMock,
|
||||
},
|
||||
settingsApi: {
|
||||
openExternal: openExternalMock,
|
||||
},
|
||||
}));
|
||||
const toastSuccessMock = vi.fn();
|
||||
const toastErrorMock = vi.fn();
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
success: toastSuccessMock,
|
||||
error: toastErrorMock,
|
||||
success: (...args: unknown[]) => toastSuccessMock(...args),
|
||||
error: (...args: unknown[]) => toastErrorMock(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) =>
|
||||
options?.defaultValue ?? key,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
vi.mock("@/components/providers/ProviderList", () => ({
|
||||
ProviderList: ({
|
||||
providers,
|
||||
@@ -153,29 +30,13 @@ vi.mock("@/components/providers/ProviderList", () => ({
|
||||
}: any) => (
|
||||
<div>
|
||||
<div data-testid="provider-list">{JSON.stringify(providers)}</div>
|
||||
<button onClick={() => onSwitch({ id: currentProviderId })}>switch</button>
|
||||
<button onClick={() => onEdit({ id: currentProviderId })}>edit</button>
|
||||
<button onClick={() => onDelete({ id: currentProviderId })}>delete</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onDuplicate(
|
||||
providers[currentProviderId] ?? {
|
||||
id: "claude-1",
|
||||
name: "Claude Default",
|
||||
settingsConfig: {},
|
||||
sortIndex: 0,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
duplicate
|
||||
</button>
|
||||
<button onClick={() => onConfigureUsage({ id: currentProviderId })}>
|
||||
usage
|
||||
</button>
|
||||
<button onClick={() => onOpenWebsite("https://example.com")}>
|
||||
open-website
|
||||
</button>
|
||||
<div data-testid="current-provider">{currentProviderId}</div>
|
||||
<button onClick={() => onSwitch(providers[currentProviderId])}>switch</button>
|
||||
<button onClick={() => onEdit(providers[currentProviderId])}>edit</button>
|
||||
<button onClick={() => onDelete(providers[currentProviderId])}>delete</button>
|
||||
<button onClick={() => onDuplicate(providers[currentProviderId])}>duplicate</button>
|
||||
<button onClick={() => onConfigureUsage(providers[currentProviderId])}>usage</button>
|
||||
<button onClick={() => onOpenWebsite("https://example.com")}>open-website</button>
|
||||
<button onClick={() => onCreate?.()}>create</button>
|
||||
</div>
|
||||
),
|
||||
@@ -185,8 +46,17 @@ vi.mock("@/components/providers/AddProviderDialog", () => ({
|
||||
AddProviderDialog: ({ open, onOpenChange, onSubmit, appType }: any) =>
|
||||
open ? (
|
||||
<div data-testid="add-provider-dialog">
|
||||
<button onClick={() => onSubmit({ name: "New Provider", appType })}>
|
||||
add-provider
|
||||
<button
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
name: `New ${appType} Provider`,
|
||||
settingsConfig: {},
|
||||
category: "custom",
|
||||
sortIndex: 99,
|
||||
})
|
||||
}
|
||||
>
|
||||
confirm-add
|
||||
</button>
|
||||
<button onClick={() => onOpenChange(false)}>close-add</button>
|
||||
</div>
|
||||
@@ -197,16 +67,15 @@ vi.mock("@/components/providers/EditProviderDialog", () => ({
|
||||
EditProviderDialog: ({ open, provider, onSubmit, onOpenChange }: any) =>
|
||||
open ? (
|
||||
<div data-testid="edit-provider-dialog">
|
||||
<span>{provider?.id}</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
id: provider?.id,
|
||||
name: `${provider?.name}-edited`,
|
||||
...provider,
|
||||
name: `${provider.name}-edited`,
|
||||
})
|
||||
}
|
||||
>
|
||||
save-edit
|
||||
confirm-edit
|
||||
</button>
|
||||
<button onClick={() => onOpenChange(false)}>close-edit</button>
|
||||
</div>
|
||||
@@ -217,7 +86,7 @@ vi.mock("@/components/UsageScriptModal", () => ({
|
||||
default: ({ isOpen, provider, onSave, onClose }: any) =>
|
||||
isOpen ? (
|
||||
<div data-testid="usage-modal">
|
||||
<span>{provider?.id}</span>
|
||||
<span data-testid="usage-provider">{provider?.id}</span>
|
||||
<button onClick={() => onSave("script-code")}>save-script</button>
|
||||
<button onClick={() => onClose()}>close-usage</button>
|
||||
</div>
|
||||
@@ -238,7 +107,7 @@ vi.mock("@/components/settings/SettingsDialog", () => ({
|
||||
SettingsDialog: ({ open, onOpenChange, onImportSuccess }: any) =>
|
||||
open ? (
|
||||
<div data-testid="settings-dialog">
|
||||
<button onClick={() => onImportSuccess?.()}>settings-on-import</button>
|
||||
<button onClick={() => onImportSuccess?.()}>trigger-import-success</button>
|
||||
<button onClick={() => onOpenChange(false)}>close-settings</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -250,7 +119,8 @@ vi.mock("@/components/AppSwitcher", () => ({
|
||||
AppSwitcher: ({ activeApp, onSwitch }: any) => (
|
||||
<div data-testid="app-switcher">
|
||||
<span>{activeApp}</span>
|
||||
<button onClick={() => onSwitch("codex")}>switch-app</button>
|
||||
<button onClick={() => onSwitch("claude")}>switch-claude</button>
|
||||
<button onClick={() => onSwitch("codex")}>switch-codex</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -272,124 +142,84 @@ vi.mock("@/components/mcp/McpPanel", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const mockRefetch = vi.fn();
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
let AppComponent: typeof import("@/App").default;
|
||||
|
||||
describe("App Integration", () => {
|
||||
beforeAll(async () => {
|
||||
const module = await import("@/App");
|
||||
AppComponent = module.default;
|
||||
});
|
||||
const renderApp = () => {
|
||||
const client = new QueryClient();
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<Suspense fallback={<div data-testid="loading">loading</div>}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe("App Integration with MSW", () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
mockRefetch.mockReset();
|
||||
mockRefetch.mockResolvedValue(undefined);
|
||||
useProvidersQueryMock.mockReset();
|
||||
getAllMock.mockReset();
|
||||
getCurrentMock.mockReset();
|
||||
importDefaultMock.mockReset();
|
||||
onSwitchedMock.mockResolvedValue(() => {});
|
||||
updateSortOrderMock.mockResolvedValue(undefined);
|
||||
updateTrayMenuMock.mockResolvedValue(undefined);
|
||||
openExternalMock.mockResolvedValue(undefined);
|
||||
|
||||
useProvidersQueryMock.mockImplementation((appType: string) => {
|
||||
const providers = providersDataMock[appType as keyof typeof providersDataMock] || [];
|
||||
getAllMock.mockResolvedValue(Object.fromEntries(providers.map((provider) => [provider.id, provider])));
|
||||
getCurrentMock.mockResolvedValue(providers[0]?.id ?? "");
|
||||
importDefaultMock.mockResolvedValue(false);
|
||||
return {
|
||||
data: {
|
||||
providers: Object.fromEntries(
|
||||
providers.map((provider) => [provider.id, provider]),
|
||||
),
|
||||
currentProviderId: providers[0]?.id ?? "",
|
||||
},
|
||||
isLoading: false,
|
||||
refetch: mockRefetch,
|
||||
};
|
||||
});
|
||||
resetProviderState();
|
||||
toastSuccessMock.mockReset();
|
||||
toastErrorMock.mockReset();
|
||||
});
|
||||
|
||||
it("should render providers, open dialogs, and execute core flows", async () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Suspense fallback={<div data-testid="loading">loading</div>}>
|
||||
<AppComponent />
|
||||
</Suspense>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
it("runs provider flows with mocked dialogs but real hooks", async () => {
|
||||
renderApp();
|
||||
|
||||
// 初始加载后,应显示 ProviderList mock 渲染的 JSON
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("provider-list")).toBeInTheDocument(),
|
||||
expect(screen.getByTestId("provider-list").textContent).toContain("claude-1"),
|
||||
);
|
||||
expect(screen.getByText("CC Switch")).toBeInTheDocument();
|
||||
|
||||
// 打开设置对话框并触发导入成功回调
|
||||
fireEvent.click(screen.getByText("update-badge")); // open settings via badge
|
||||
fireEvent.click(screen.getByText("update-badge"));
|
||||
expect(screen.getByTestId("settings-dialog")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("settings-on-import"));
|
||||
await waitFor(() => expect(mockRefetch).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(updateTrayMenuMock).toHaveBeenCalledTimes(1));
|
||||
fireEvent.click(screen.getByText("trigger-import-success"));
|
||||
fireEvent.click(screen.getByText("close-settings"));
|
||||
expect(screen.queryByTestId("settings-dialog")).not.toBeInTheDocument();
|
||||
|
||||
// 切换到 codex 应用,确保 useProvidersQuery 被使用
|
||||
fireEvent.click(screen.getByText("switch-app"));
|
||||
await waitFor(() => {
|
||||
expect(useProvidersQueryMock).toHaveBeenCalledWith("codex");
|
||||
});
|
||||
fireEvent.click(screen.getByText("switch-codex"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("provider-list").textContent).toContain("codex-1"),
|
||||
);
|
||||
|
||||
// 添加供应商流程
|
||||
fireEvent.click(screen.getByText("header.addProvider"));
|
||||
fireEvent.click(screen.getByText("duplicate"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("provider-list").textContent).toMatch(/copy/),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("create"));
|
||||
expect(screen.getByTestId("add-provider-dialog")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("add-provider"));
|
||||
expect(addProviderMock).toHaveBeenCalledWith({ name: "New Provider", appType: "codex" });
|
||||
fireEvent.click(screen.getByText("close-add"));
|
||||
fireEvent.click(screen.getByText("confirm-add"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("provider-list").textContent).toMatch(/New codex Provider/),
|
||||
);
|
||||
|
||||
// 编辑供应商流程
|
||||
fireEvent.click(screen.getByText("edit"));
|
||||
expect(screen.getByTestId("edit-provider-dialog")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("save-edit"));
|
||||
expect(updateProviderMock).toHaveBeenCalledWith({
|
||||
id: "codex-1",
|
||||
name: "undefined-edited",
|
||||
});
|
||||
fireEvent.click(screen.getByText("close-edit"));
|
||||
fireEvent.click(screen.getByText("confirm-edit"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("provider-list").textContent).toMatch(/-edited/),
|
||||
);
|
||||
|
||||
// 删除供应商流程
|
||||
fireEvent.click(screen.getByText("delete"));
|
||||
expect(screen.getByTestId("confirm-dialog")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("confirm-delete"));
|
||||
expect(deleteProviderMock).toHaveBeenCalledWith("codex-1");
|
||||
|
||||
// 复制供应商流程(触发排序更新 + 添加)
|
||||
fireEvent.click(screen.getByText("duplicate"));
|
||||
await waitFor(() => {
|
||||
expect(updateSortOrderMock).toHaveBeenCalled();
|
||||
});
|
||||
expect(addProviderMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
// 使用脚本弹窗
|
||||
fireEvent.click(screen.getByText("usage"));
|
||||
expect(screen.getByTestId("usage-modal")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("save-script"));
|
||||
expect(saveUsageScriptMock).toHaveBeenCalledWith(
|
||||
{ id: "codex-1" },
|
||||
"script-code",
|
||||
);
|
||||
fireEvent.click(screen.getByText("close-usage"));
|
||||
expect(screen.queryByTestId("usage-modal")).not.toBeInTheDocument();
|
||||
|
||||
// 打开网站链接
|
||||
fireEvent.click(screen.getByText("delete"));
|
||||
expect(screen.getByTestId("confirm-dialog")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText("confirm-delete"));
|
||||
await waitFor(() =>
|
||||
expect(Object.keys(listProviders("codex"))).not.toContain("codex-1"),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("current-provider").textContent).not.toBe("codex-1"),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("open-website"));
|
||||
expect(openExternalMock).toHaveBeenCalledWith("https://example.com");
|
||||
|
||||
// 确保页面保留核心元素
|
||||
expect(container.textContent).toContain("CC Switch");
|
||||
emitTauriEvent("provider-switched", { appType: "codex", providerId: "codex-2" });
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("current-provider").textContent).toBe("codex-2"),
|
||||
);
|
||||
|
||||
expect(toastSuccessMock).toHaveBeenCalled();
|
||||
expect(toastErrorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
100
tests/msw/handlers.ts
Normal file
100
tests/msw/handlers.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
import type { AppType } from "@/lib/api/types";
|
||||
import type { Provider } from "@/types";
|
||||
import {
|
||||
addProvider,
|
||||
deleteProvider,
|
||||
getCurrentProviderId,
|
||||
getProviders,
|
||||
listProviders,
|
||||
resetProviderState,
|
||||
setCurrentProviderId,
|
||||
updateProvider,
|
||||
updateSortOrder,
|
||||
} from "./state";
|
||||
|
||||
const TAURI_ENDPOINT = "http://tauri.local";
|
||||
|
||||
const withJson = async <T>(request: Request): Promise<T> => {
|
||||
try {
|
||||
const body = await request.text();
|
||||
if (!body) return {} as T;
|
||||
return JSON.parse(body) as T;
|
||||
} catch {
|
||||
return {} as T;
|
||||
}
|
||||
};
|
||||
|
||||
const success = <T>(payload: T) => HttpResponse.json(payload as any);
|
||||
|
||||
export const handlers = [
|
||||
http.post(`${TAURI_ENDPOINT}/get_providers`, async ({ request }) => {
|
||||
const { app_type } = await withJson<{ app_type: AppType }>(request);
|
||||
return success(getProviders(app_type));
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/get_current_provider`, async ({ request }) => {
|
||||
const { app_type } = await withJson<{ app_type: AppType }>(request);
|
||||
return success(getCurrentProviderId(app_type));
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/update_providers_sort_order`, async ({ request }) => {
|
||||
const { updates = [], app_type } = await withJson<{
|
||||
updates: { id: string; sortIndex: number }[];
|
||||
app_type: AppType;
|
||||
}>(request);
|
||||
updateSortOrder(app_type, updates);
|
||||
return success(true);
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/update_tray_menu`, () => success(true)),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/switch_provider`, async ({ request }) => {
|
||||
const { id, app_type } = await withJson<{ id: string; app_type: AppType }>(
|
||||
request,
|
||||
);
|
||||
const providers = listProviders(app_type);
|
||||
if (!providers[id]) {
|
||||
return HttpResponse.json(false, { status: 404 });
|
||||
}
|
||||
setCurrentProviderId(app_type, id);
|
||||
return success(true);
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/add_provider`, async ({ request }) => {
|
||||
const { provider, app_type } = await withJson<{
|
||||
provider: Provider & { id?: string };
|
||||
app_type: AppType;
|
||||
}>(request);
|
||||
|
||||
const newId = provider.id ?? `mock-${Date.now()}`;
|
||||
addProvider(app_type, { ...provider, id: newId });
|
||||
return success(true);
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/update_provider`, async ({ request }) => {
|
||||
const { provider, app_type } = await withJson<{
|
||||
provider: Provider;
|
||||
app_type: AppType;
|
||||
}>(request);
|
||||
updateProvider(app_type, provider);
|
||||
return success(true);
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/delete_provider`, async ({ request }) => {
|
||||
const { id, app_type } = await withJson<{ id: string; app_type: AppType }>(
|
||||
request,
|
||||
);
|
||||
deleteProvider(app_type, id);
|
||||
return success(true);
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/import_default_config`, async () => {
|
||||
resetProviderState();
|
||||
return success(true);
|
||||
}),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/open_external`, () => success(true)),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/restart_app`, () => success(true)),
|
||||
];
|
||||
5
tests/msw/server.ts
Normal file
5
tests/msw/server.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { setupServer } from "msw/node";
|
||||
import { handlers } from "./handlers";
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
|
||||
116
tests/msw/state.ts
Normal file
116
tests/msw/state.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { AppType } from "@/lib/api/types";
|
||||
import type { Provider } from "@/types";
|
||||
|
||||
type ProvidersByApp = Record<AppType, Record<string, Provider>>;
|
||||
type CurrentProviderState = Record<AppType, string>;
|
||||
|
||||
const createDefaultProviders = (): ProvidersByApp => ({
|
||||
claude: {
|
||||
"claude-1": {
|
||||
id: "claude-1",
|
||||
name: "Claude Default",
|
||||
settingsConfig: {},
|
||||
category: "official",
|
||||
sortIndex: 0,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
"claude-2": {
|
||||
id: "claude-2",
|
||||
name: "Claude Custom",
|
||||
settingsConfig: {},
|
||||
category: "custom",
|
||||
sortIndex: 1,
|
||||
createdAt: Date.now() + 1,
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
"codex-1": {
|
||||
id: "codex-1",
|
||||
name: "Codex Default",
|
||||
settingsConfig: {},
|
||||
category: "official",
|
||||
sortIndex: 0,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
"codex-2": {
|
||||
id: "codex-2",
|
||||
name: "Codex Secondary",
|
||||
settingsConfig: {},
|
||||
category: "custom",
|
||||
sortIndex: 1,
|
||||
createdAt: Date.now() + 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createDefaultCurrent = (): CurrentProviderState => ({
|
||||
claude: "claude-1",
|
||||
codex: "codex-1",
|
||||
});
|
||||
|
||||
let providers = createDefaultProviders();
|
||||
let current = createDefaultCurrent();
|
||||
|
||||
const cloneProviders = (value: ProvidersByApp) =>
|
||||
JSON.parse(JSON.stringify(value)) as ProvidersByApp;
|
||||
|
||||
export const resetProviderState = () => {
|
||||
providers = createDefaultProviders();
|
||||
current = createDefaultCurrent();
|
||||
};
|
||||
|
||||
export const getProviders = (appType: AppType) =>
|
||||
cloneProviders(providers)[appType] ?? {};
|
||||
|
||||
export const getCurrentProviderId = (appType: AppType) => current[appType] ?? "";
|
||||
|
||||
export const setCurrentProviderId = (appType: AppType, providerId: string) => {
|
||||
current[appType] = providerId;
|
||||
};
|
||||
|
||||
export const updateProviders = (appType: AppType, data: Record<string, Provider>) => {
|
||||
providers[appType] = cloneProviders({ [appType]: data } as ProvidersByApp)[appType];
|
||||
};
|
||||
|
||||
export const setProviders = (appType: AppType, data: Record<string, Provider>) => {
|
||||
providers[appType] = JSON.parse(JSON.stringify(data)) as Record<string, Provider>;
|
||||
};
|
||||
|
||||
export const addProvider = (appType: AppType, provider: Provider) => {
|
||||
providers[appType] = providers[appType] ?? {};
|
||||
providers[appType][provider.id] = provider;
|
||||
};
|
||||
|
||||
export const updateProvider = (appType: AppType, provider: Provider) => {
|
||||
if (!providers[appType]) return;
|
||||
providers[appType][provider.id] = {
|
||||
...providers[appType][provider.id],
|
||||
...provider,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteProvider = (appType: AppType, providerId: string) => {
|
||||
if (!providers[appType]) return;
|
||||
delete providers[appType][providerId];
|
||||
if (current[appType] === providerId) {
|
||||
const fallback = Object.keys(providers[appType])[0] ?? "";
|
||||
current[appType] = fallback;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSortOrder = (
|
||||
appType: AppType,
|
||||
updates: { id: string; sortIndex: number }[],
|
||||
) => {
|
||||
if (!providers[appType]) return;
|
||||
updates.forEach(({ id, sortIndex }) => {
|
||||
const provider = providers[appType][id];
|
||||
if (provider) {
|
||||
providers[appType][id] = { ...provider, sortIndex };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const listProviders = (appType: AppType) =>
|
||||
JSON.parse(JSON.stringify(providers[appType] ?? {})) as Record<string, Provider>;
|
||||
|
||||
61
tests/msw/tauriMocks.ts
Normal file
61
tests/msw/tauriMocks.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import "cross-fetch/polyfill";
|
||||
import { vi } from "vitest";
|
||||
import { server } from "./server";
|
||||
|
||||
const TAURI_ENDPOINT = "http://tauri.local";
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: async (command: string, payload: Record<string, unknown> = {}) => {
|
||||
const response = await fetch(`${TAURI_ENDPOINT}/${command}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload ?? {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || `Invoke failed for ${command}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!text) return undefined;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const listeners = new Map<
|
||||
string,
|
||||
Set<(event: { payload: unknown }) => void>
|
||||
>();
|
||||
|
||||
const ensureListenerSet = (event: string) => {
|
||||
if (!listeners.has(event)) {
|
||||
listeners.set(event, new Set());
|
||||
}
|
||||
return listeners.get(event)!;
|
||||
};
|
||||
|
||||
export const emitTauriEvent = (event: string, payload: unknown) => {
|
||||
const handlers = listeners.get(event);
|
||||
handlers?.forEach((handler) => handler({ payload }));
|
||||
};
|
||||
|
||||
vi.mock("@tauri-apps/api/event", () => ({
|
||||
listen: async (event: string, handler: (event: { payload: unknown }) => void) => {
|
||||
const set = ensureListenerSet(event);
|
||||
set.add(handler);
|
||||
return () => {
|
||||
set.delete(handler);
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
// Ensure the MSW server is referenced so tree shaking doesn't remove imports
|
||||
void server;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { afterEach, beforeAll } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, vi } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { server } from "./msw/server";
|
||||
import { resetProviderState } from "./msw/state";
|
||||
import "./msw/tauriMocks";
|
||||
|
||||
beforeAll(async () => {
|
||||
server.listen({ onUnhandledRequest: "warn" });
|
||||
await i18n.use(initReactI18next).init({
|
||||
lng: "zh",
|
||||
fallbackLng: "zh",
|
||||
@@ -20,4 +24,11 @@ beforeAll(async () => {
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
resetProviderState();
|
||||
server.resetHandlers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user