diff --git a/package.json b/package.json index e60aa3c..9cef59e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 292b0cc..ed7d06b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/tests/integration/App.test.tsx b/tests/integration/App.test.tsx index 6b4d050..0d6c53a 100644 --- a/tests/integration/App.test.tsx +++ b/tests/integration/App.test.tsx @@ -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("react-i18next"); - return { - ...actual, - useTranslation: () => ({ - t: (key: string, options?: Record) => - options?.defaultValue ?? key, - }), - }; -}); - - vi.mock("@/components/providers/ProviderList", () => ({ ProviderList: ({ providers, @@ -153,29 +30,13 @@ vi.mock("@/components/providers/ProviderList", () => ({ }: any) => (
{JSON.stringify(providers)}
- - - - - - +
{currentProviderId}
+ + + + + +
), @@ -185,8 +46,17 @@ vi.mock("@/components/providers/AddProviderDialog", () => ({ AddProviderDialog: ({ open, onOpenChange, onSubmit, appType }: any) => open ? (
-
@@ -197,16 +67,15 @@ vi.mock("@/components/providers/EditProviderDialog", () => ({ EditProviderDialog: ({ open, provider, onSubmit, onOpenChange }: any) => open ? (
- {provider?.id}
@@ -217,7 +86,7 @@ vi.mock("@/components/UsageScriptModal", () => ({ default: ({ isOpen, provider, onSave, onClose }: any) => isOpen ? (
- {provider?.id} + {provider?.id}
@@ -238,7 +107,7 @@ vi.mock("@/components/settings/SettingsDialog", () => ({ SettingsDialog: ({ open, onOpenChange, onImportSuccess }: any) => open ? (
- +
) : ( @@ -250,7 +119,8 @@ vi.mock("@/components/AppSwitcher", () => ({ AppSwitcher: ({ activeApp, onSwitch }: any) => (
{activeApp} - + +
), })); @@ -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( + + loading}> + + + , + ); +}; +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( - - loading}> - - - , - ); + 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(); }); }); diff --git a/tests/msw/handlers.ts b/tests/msw/handlers.ts new file mode 100644 index 0000000..de10561 --- /dev/null +++ b/tests/msw/handlers.ts @@ -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 (request: Request): Promise => { + try { + const body = await request.text(); + if (!body) return {} as T; + return JSON.parse(body) as T; + } catch { + return {} as T; + } +}; + +const success = (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)), +]; diff --git a/tests/msw/server.ts b/tests/msw/server.ts new file mode 100644 index 0000000..a07068d --- /dev/null +++ b/tests/msw/server.ts @@ -0,0 +1,5 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; + +export const server = setupServer(...handlers); + diff --git a/tests/msw/state.ts b/tests/msw/state.ts new file mode 100644 index 0000000..ce70520 --- /dev/null +++ b/tests/msw/state.ts @@ -0,0 +1,116 @@ +import type { AppType } from "@/lib/api/types"; +import type { Provider } from "@/types"; + +type ProvidersByApp = Record>; +type CurrentProviderState = Record; + +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) => { + providers[appType] = cloneProviders({ [appType]: data } as ProvidersByApp)[appType]; +}; + +export const setProviders = (appType: AppType, data: Record) => { + providers[appType] = JSON.parse(JSON.stringify(data)) as Record; +}; + +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; + diff --git a/tests/msw/tauriMocks.ts b/tests/msw/tauriMocks.ts new file mode 100644 index 0000000..5d086d4 --- /dev/null +++ b/tests/msw/tauriMocks.ts @@ -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 = {}) => { + 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; + diff --git a/tests/setupTests.ts b/tests/setupTests.ts index 6a624c7..35bea33 100644 --- a/tests/setupTests.ts +++ b/tests/setupTests.ts @@ -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(); });