diff --git a/CHANGELOG.md b/CHANGELOG.md index 22afc5f..a4bc74e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @tobelabs/chainwright +## 0.10.13 + +### Patch Changes + +- [CLI] - Enhance error handling in downloadFile function. + ## 0.10.12 ### Patch Changes diff --git a/package.json b/package.json index 7c70791..4d492db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chainwright", - "version": "0.10.12", + "version": "0.10.13", "description": "Playwright Web3 wallet testing framework for end-to-end dApp automation with MetaMask, Phantom, Solflare, Petra, Meteor, and Keplr", "type": "module", "license": "MIT", @@ -95,14 +95,14 @@ }, "devDependencies": { "@biomejs/biome": "2.4.15", - "@changesets/cli": "^2.31.0", + "@changesets/cli": "^2.31.1", "@playwright/test": "1.61.1", "@types/adm-zip": "^0.5.8", "@types/cli-progress": "^3.11.6", "@types/node": "^26.0.1", "tsup": "^8.5.1", "typescript": "^6.0.3", - "vitest": "^4.1.9" + "vitest": "^4.1.10" }, "dependencies": { "@inquirer/checkbox": "^5.2.1", @@ -110,19 +110,19 @@ "cli-progress": "^3.12.0", "commander": "^15.0.0", "glob": "^13.0.6", - "prool": "^0.2.4", - "tsx": "^4.22.4", + "prool": "^0.2.14", + "tsx": "^4.23.9", "zod": "^4.4.3" }, "overrides": { - "tar": "^7.5.15" + "tar": "^7.5.21" }, "resolutions": { - "tar": "^7.5.15" + "tar": "^7.5.21" }, "pnpm": { "overrides": { - "tar": "^7.5.15" + "tar": "^7.5.21" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1031324..5059d83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,11 +27,11 @@ importers: specifier: ^13.0.6 version: 13.0.6 prool: - specifier: ^0.2.4 - version: 0.2.4 + specifier: ^0.2.14 + version: 0.2.14(debug@4.4.3) tsx: - specifier: ^4.22.4 - version: 4.22.4 + specifier: ^4.23.9 + version: 4.23.9 zod: specifier: ^4.4.3 version: 4.4.3 @@ -40,8 +40,8 @@ importers: specifier: 2.4.15 version: 2.4.15 '@changesets/cli': - specifier: ^2.31.0 - version: 2.31.0(@types/node@26.0.1) + specifier: ^2.31.1 + version: 2.31.1(@types/node@26.0.1) '@playwright/test': specifier: 1.61.1 version: 1.61.1 @@ -56,13 +56,13 @@ importers: version: 26.0.1 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.22)(tsx@4.22.4)(typescript@6.0.3) + version: 8.5.1(postcss@8.5.22)(tsx@4.23.9)(typescript@6.0.3) typescript: specifier: ^6.0.3 version: 6.0.3 vitest: - specifier: ^4.1.9 - version: 4.1.9(@types/node@26.0.1)(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.22.4)) + specifier: ^4.1.10 + version: 4.1.10(@types/node@26.0.1)(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.23.9)) packages: @@ -136,8 +136,8 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.31.0': - resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} hasBin: true '@changesets/config@3.1.4': @@ -872,11 +872,11 @@ packages: '@types/node@26.0.1': resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} - '@vitest/expect@4.1.9': - resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.9': - resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -886,20 +886,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.9': - resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.9': - resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.9': - resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.9': - resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.9': - resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} @@ -1511,8 +1511,8 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - prool@0.2.4: - resolution: {integrity: sha512-KAGs6e++7MJNQ/vq8Xrk6akz0lRk6AmhuGzSHkluX3kwVj2XjNDDOYSINZwahRv3xfSD0rXYv3iA/2vXw7z47w==} + prool@0.2.14: + resolution: {integrity: sha512-GAu9hAJqIMac/16sHTm//ya8qfqwh0M+czyRcQYlL8daXYHhSfxOCbNv9SFkmRQ1wZhZHDvDtfqNJZmCcwzJaw==} engines: {node: '>=22'} peerDependencies: '@pimlico/alto': '*' @@ -1629,8 +1629,8 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - tar@7.5.15: - resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} term-size@2.2.1: @@ -1699,8 +1699,8 @@ packages: typescript: optional: true - tsx@4.22.4: - resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + tsx@4.23.9: + resolution: {integrity: sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw==} engines: {node: '>=18.0.0'} hasBin: true @@ -1769,20 +1769,20 @@ packages: yaml: optional: true - vitest@4.1.9: - resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.9 - '@vitest/browser-preview': 4.1.9 - '@vitest/browser-webdriverio': 4.1.9 - '@vitest/coverage-istanbul': 4.1.9 - '@vitest/coverage-v8': 4.1.9 - '@vitest/ui': 4.1.9 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1899,7 +1899,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.0(@types/node@26.0.1)': + '@changesets/cli@2.31.1(@types/node@26.0.1)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -2446,44 +2446,44 @@ snapshots: dependencies: undici-types: 8.3.0 - '@vitest/expect@4.1.9': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.22.4))': + '@vitest/mocker@4.1.10(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.23.9))': dependencies: - '@vitest/spy': 4.1.9 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.22.4) + vite: 8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.23.9) - '@vitest/pretty-format@4.1.9': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.9': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.9 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.9': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.9': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.9': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.9 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -2719,7 +2719,9 @@ snapshots: mlly: 1.8.2 rollup: 4.60.4 - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 fs-extra@7.0.1: dependencies: @@ -2767,10 +2769,10 @@ snapshots: graceful-fs@4.2.11: {} - http-proxy@1.18.1: + http-proxy@1.18.1(debug@4.4.3): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -3003,12 +3005,12 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - postcss-load-config@6.0.1(postcss@8.5.22)(tsx@4.22.4): + postcss-load-config@6.0.1(postcss@8.5.22)(tsx@4.23.9): dependencies: lilconfig: 3.1.3 optionalDependencies: postcss: 8.5.22 - tsx: 4.22.4 + tsx: 4.23.9 postcss@8.5.22: dependencies: @@ -3022,14 +3024,14 @@ snapshots: dependencies: parse-ms: 4.0.0 - prool@0.2.4: + prool@0.2.14(debug@4.4.3): dependencies: change-case: 5.4.4 eventemitter3: 5.0.4 execa: 9.6.1 get-port: 7.2.0 - http-proxy: 1.18.1 - tar: 7.5.15 + http-proxy: 1.18.1(debug@4.4.3) + tar: 7.5.22 transitivePeerDependencies: - debug @@ -3163,7 +3165,7 @@ snapshots: tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 - tar@7.5.15: + tar@7.5.22: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -3210,7 +3212,7 @@ snapshots: tslib@2.8.1: optional: true - tsup@8.5.1(postcss@8.5.22)(tsx@4.22.4)(typescript@6.0.3): + tsup@8.5.1(postcss@8.5.22)(tsx@4.23.9)(typescript@6.0.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -3221,7 +3223,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.22)(tsx@4.22.4) + postcss-load-config: 6.0.1(postcss@8.5.22)(tsx@4.23.9) resolve-from: 5.0.0 rollup: 4.60.4 source-map: 0.7.6 @@ -3238,7 +3240,7 @@ snapshots: - tsx - yaml - tsx@4.22.4: + tsx@4.23.9: dependencies: esbuild: 0.28.0 optionalDependencies: @@ -3256,7 +3258,7 @@ snapshots: universalify@0.1.2: {} - vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.22.4): + vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.23.9): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -3267,29 +3269,29 @@ snapshots: '@types/node': 26.0.1 esbuild: 0.27.7 fsevents: 2.3.3 - tsx: 4.22.4 + tsx: 4.23.9 - vitest@4.1.9(@types/node@26.0.1)(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.22.4)): + vitest@4.1.10(@types/node@26.0.1)(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.23.9)): dependencies: - '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.22.4)) - '@vitest/pretty-format': 4.1.9 - '@vitest/runner': 4.1.9 - '@vitest/snapshot': 4.1.9 - '@vitest/spy': 4.1.9 - '@vitest/utils': 4.1.9 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.23.9)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.4 + picomatch: 4.0.5 std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.1.2 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.22.4) + vite: 8.0.13(@types/node@26.0.1)(esbuild@0.27.7)(tsx@4.23.9) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.0.1 diff --git a/src/cli/entry-point.ts b/src/cli/entry-point.ts index c8c6526..e7377f6 100644 --- a/src/cli/entry-point.ts +++ b/src/cli/entry-point.ts @@ -103,6 +103,7 @@ export async function clientEntry() { selectedWallets: response as Array, }); + let ACTION_COUNT = 0; for (const { walletName, config, walletPassword, setupFunction, fileList } of _setupFunction) { try { console.info( @@ -171,11 +172,15 @@ export async function clientEntry() { { validateStream: false }, ), ); + if (_setupFunction.length === 1 || ACTION_COUNT === _setupFunction.length - 1) { + process.exit(1); + } } } } } } + ACTION_COUNT++; } }); diff --git a/src/utils/download-file.ts b/src/utils/download-file.ts index 8214170..3641721 100644 --- a/src/utils/download-file.ts +++ b/src/utils/download-file.ts @@ -17,9 +17,15 @@ export async function downloadFile({ url, destination }: DownloadFileArgs) { const response = await fetch(url, { redirect: "follow", signal: controller.signal }); if (!response.ok) { - console.error(styleText("redBright", `❌ Download failed: HTTP ${response.status}`, { validateStream: false })); + const errorBody = await response.text().catch(() => ""); + console.error( + styleText( + "redBright", + `❌ Download failed: HTTP ${response.status} ${response.statusText}${errorBody ? `\n${errorBody.slice(0, 500)}` : ""}`, + { validateStream: false }, + ), + ); controller.abort(); - process.exit(1); } const totalBytes = parseInt(response.headers.get("content-length") || "0", 10); @@ -53,7 +59,6 @@ export async function downloadFile({ url, destination }: DownloadFileArgs) { }); } catch (error) { console.error(styleText("redBright", `❌ Download failed: ${error}`, { validateStream: false })); - process.exit(1); } finally { clearTimeout(requestTimeout); } diff --git a/src/utils/tests/download-file.test.ts b/src/utils/tests/download-file.test.ts index 160f8cd..b43ac37 100644 --- a/src/utils/tests/download-file.test.ts +++ b/src/utils/tests/download-file.test.ts @@ -114,7 +114,7 @@ describe("downloadFile", () => { expect(mockProgressBar.stop).toHaveBeenCalled(); }); - it("should exit with error code 1 when HTTP request fails", async () => { + it("should log the status, status text and error body when the HTTP request fails", async () => { const url = "https://example.com/not-found.txt"; const destination = path.resolve(TEST_DIR, "not-found.txt"); @@ -122,21 +122,70 @@ describe("downloadFile", () => { global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404, + statusText: "Not Found", headers: new Headers(), + text: vi.fn().mockResolvedValue("Resource not found"), }); - const exitSpy = vi.spyOn(process, "exit").mockImplementation((code) => { - expect(code).toBe(1); - throw new Error("process.exit called"); + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + await expect(downloadFile({ url, destination })).rejects.toThrow(); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("❌ Download failed: HTTP 404 Not Found")); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Resource not found")); + + // Verify the in-flight request was aborted + const fetchMock = global.fetch as unknown as ReturnType; + const fetchOptions = fetchMock.mock.calls[0]?.[1] as { signal: AbortSignal }; + expect(fetchOptions.signal.aborted).toBe(true); + + consoleErrorSpy.mockRestore(); + }); + + it("should truncate the error body to 500 characters when the HTTP request fails", async () => { + const url = "https://example.com/server-error.txt"; + const destination = path.resolve(TEST_DIR, "server-error.txt"); + const longBody = "x".repeat(1000); + + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + headers: new Headers(), + text: vi.fn().mockResolvedValue(longBody), }); const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - await expect(downloadFile({ url, destination })).rejects.toThrow("process.exit called"); + await expect(downloadFile({ url, destination })).rejects.toThrow(); - expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(`❌ Download failed: HTTP 404`)); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("x".repeat(500))); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining("x".repeat(501))); + + consoleErrorSpy.mockRestore(); + }); + + it("should omit the error body when reading it fails", async () => { + const url = "https://example.com/unreadable-body.txt"; + const destination = path.resolve(TEST_DIR, "unreadable-body.txt"); + + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 502, + statusText: "Bad Gateway", + headers: new Headers(), + text: vi.fn().mockRejectedValue(new Error("body stream error")), + }); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + await expect(downloadFile({ url, destination })).rejects.toThrow(); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("❌ Download failed: HTTP 502 Bad Gateway"), + ); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining("\n")); - exitSpy.mockRestore(); consoleErrorSpy.mockRestore(); }); @@ -153,10 +202,6 @@ describe("downloadFile", () => { body: Readable.toWeb(Readable.from(contentBuffer)) as ReadableStream, }); - const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit called"); - }); - await expect(downloadFile({ url, destination })).resolves.toBeUndefined(); // Get the progress bar instance that was created @@ -171,8 +216,6 @@ describe("downloadFile", () => { expect(fs.existsSync(destination)).toBe(true); const fileContent = fs.readFileSync(destination, "utf-8"); expect(fileContent).toBe(content); - - exitSpy.mockRestore(); }); it("should handle timeout and abort the request", async () => { @@ -184,12 +227,33 @@ describe("downloadFile", () => { abortError.name = "AbortError"; global.fetch = vi.fn().mockRejectedValue(abortError); - const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit called"); + await expect(downloadFile({ url, destination })).rejects.toThrow("The operation was aborted"); + }); + + it("should log the error and resolve when writing to the destination fails", async () => { + const url = "https://example.com/file.txt"; + // Destination inside a directory that does not exist, so the write stream errors + const destination = path.resolve(TEST_DIR, "missing-dir", "file.txt"); + const contentBuffer = Buffer.from("Hello, World!"); + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: new Headers({ + "content-length": contentBuffer.length.toString(), + }), + body: Readable.toWeb(Readable.from(contentBuffer)) as ReadableStream, }); - await expect(downloadFile({ url, destination })).rejects.toThrow(); + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const clearTimeoutSpy = vi.spyOn(global, "clearTimeout"); - exitSpy.mockRestore(); + await expect(downloadFile({ url, destination })).resolves.toBeUndefined(); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("❌ Download failed:")); + expect(clearTimeoutSpy).toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + clearTimeoutSpy.mockRestore(); }); });