From 18927e38e88ec3a60ccc05249a6ab663755d7ee7 Mon Sep 17 00:00:00 2001 From: Ugwuanyi TobeChukwu <39131739+amaify@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:45:24 +0100 Subject: [PATCH] [Core] - Fix the issue in the `downloadFile` function --- CHANGELOG.md | 6 +++ package.json | 2 +- src/utils/download-file.ts | 30 +++++------ src/utils/tests/download-file.test.ts | 74 ++++++--------------------- 4 files changed, 35 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4bc74e..32b7a8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @tobelabs/chainwright +## 0.10.14 + +### Patch Changes + +- [Core] - Fix the error handling issue in the `downloadFile` function. + ## 0.10.13 ### Patch Changes diff --git a/package.json b/package.json index 4d492db..e2893db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chainwright", - "version": "0.10.13", + "version": "0.10.14", "description": "Playwright Web3 wallet testing framework for end-to-end dApp automation with MetaMask, Phantom, Solflare, Petra, Meteor, and Keplr", "type": "module", "license": "MIT", diff --git a/src/utils/download-file.ts b/src/utils/download-file.ts index 3641721..37a4a8f 100644 --- a/src/utils/download-file.ts +++ b/src/utils/download-file.ts @@ -14,26 +14,19 @@ type DownloadFileArgs = { export async function downloadFile({ url, destination }: DownloadFileArgs) { const controller = new AbortController(); const requestTimeout = setTimeout(() => controller.abort(), TIMEOUT); - const response = await fetch(url, { redirect: "follow", signal: controller.signal }); - if (!response.ok) { - 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(); - } + try { + const response = await fetch(url, { redirect: "follow", signal: controller.signal }); - const totalBytes = parseInt(response.headers.get("content-length") || "0", 10); - let downloaded = 0; + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } - const nodeStream = Readable.fromWeb(response.body as streamWeb.ReadableStream); + const totalBytes = parseInt(response.headers.get("content-length") || "0", 10); + let downloaded = 0; + + const nodeStream = Readable.fromWeb(response.body as streamWeb.ReadableStream); - try { const progressBar = new cliProgress.SingleBar({ format: `Downloading ${styleText("cyan", "{bar}", { validateStream: false })} {percentage}%`, clearOnComplete: true, @@ -58,7 +51,10 @@ export async function downloadFile({ url, destination }: DownloadFileArgs) { }); }); } catch (error) { - console.error(styleText("redBright", `❌ Download failed: ${error}`, { validateStream: false })); + console.error( + styleText("redBright", `❌ Wallet extension download failed: ${error}`, { validateStream: false }), + ); + throw error; } finally { clearTimeout(requestTimeout); } diff --git a/src/utils/tests/download-file.test.ts b/src/utils/tests/download-file.test.ts index b43ac37..e20205a 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 log the status, status text and error body when the HTTP request fails", async () => { + it("should log the status and status text and rethrow when the HTTP request fails", async () => { const url = "https://example.com/not-found.txt"; const destination = path.resolve(TEST_DIR, "not-found.txt"); @@ -124,67 +124,15 @@ describe("downloadFile", () => { status: 404, statusText: "Not Found", headers: new Headers(), - text: vi.fn().mockResolvedValue("Resource not found"), }); 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(); - - 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(); + await expect(downloadFile({ url, destination })).rejects.toThrow("HTTP 404 Not Found"); expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining("❌ Download failed: HTTP 502 Bad Gateway"), + expect.stringContaining("❌ Wallet extension download failed: Error: HTTP 404 Not Found"), ); - expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining("\n")); consoleErrorSpy.mockRestore(); }); @@ -218,7 +166,7 @@ describe("downloadFile", () => { expect(fileContent).toBe(content); }); - it("should handle timeout and abort the request", async () => { + it("should log the error and rethrow when the request is aborted", async () => { const url = "https://example.com/slow-file.txt"; const destination = path.resolve(TEST_DIR, "slow-file.txt"); @@ -227,10 +175,18 @@ describe("downloadFile", () => { abortError.name = "AbortError"; global.fetch = vi.fn().mockRejectedValue(abortError); + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + await expect(downloadFile({ url, destination })).rejects.toThrow("The operation was aborted"); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("❌ Wallet extension download failed: AbortError: The operation was aborted"), + ); + + consoleErrorSpy.mockRestore(); }); - it("should log the error and resolve when writing to the destination fails", async () => { + it("should log the error and rethrow 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"); @@ -248,9 +204,9 @@ describe("downloadFile", () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const clearTimeoutSpy = vi.spyOn(global, "clearTimeout"); - await expect(downloadFile({ url, destination })).resolves.toBeUndefined(); + await expect(downloadFile({ url, destination })).rejects.toThrow(); - expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("❌ Download failed:")); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("❌ Wallet extension download failed:")); expect(clearTimeoutSpy).toHaveBeenCalled(); consoleErrorSpy.mockRestore();