Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
30 changes: 13 additions & 17 deletions src/utils/download-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
}
Expand Down
74 changes: 15 additions & 59 deletions src/utils/tests/download-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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<typeof vi.fn>;
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();
});
Expand Down Expand Up @@ -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");

Expand All @@ -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");
Expand All @@ -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();
Expand Down
Loading