/* Copyright 2026 Marimo. All rights reserved. */
import { act, renderHook } from "@testing-library/react";
import { createStore, Provider } from "jotai";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MockRequestClient } from "@/__mocks__/requests";
import { toast } from "@/components/ui/use-toast";
import { requestClientAtom } from "@/core/network/requests";
import type { PackageOperationResponse } from "@/core/network/types";
import { usePackageAction } from "../usePackageAction";
vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() }));
function setup(action: "upgrade" | "remove") {
const client = MockRequestClient.create();
const store = createStore();
store.set(requestClientAtom, client);
return {
request: vi.mocked(
action === "upgrade" ? client.addPackage : client.removePackage,
),
...renderHook(
() =>
usePackageAction(action, "numpy", [{ kind: "group", value: "dev" }]),
{
wrapper: ({ children }) => (
{children}
),
},
),
};
}
describe("usePackageAction", () => {
beforeEach(() => vi.clearAllMocks());
it.each([
{ action: "upgrade", title: "Package upgraded", upgrade: true },
{ action: "remove", title: "Package removed" },
] as const)(
"preserves dependency groups and prevents duplicate $action requests",
async ({ action, title, ...options }) => {
const { result, request } = setup(action);
const { promise, resolve } =
Promise.withResolvers();
request.mockReturnValue(promise);
let pending: Promise;
act(() => {
pending = result.current.run();
});
expect(result.current.loading).toBe(true);
await act(async () => {
await result.current.run();
});
expect(request).toHaveBeenCalledExactlyOnceWith({
package: "numpy",
group: "dev",
...options,
});
await act(async () => {
resolve({ success: true });
await pending;
});
expect(result.current.loading).toBe(false);
expect(toast).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ title }),
);
},
);
it("reports saved changes requiring a restart without claiming success", async () => {
const { result, request } = setup("upgrade");
request.mockResolvedValue({ success: false, restartRequired: true });
await act(async () => {
await result.current.run();
});
expect(toast).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
title: "Changes saved — restart required",
}),
);
});
it.each([
{
response: { success: false, error: "resolver failed" },
description: "resolver failed",
},
{ response: { success: false }, description: expect.any(String) },
])(
"reports failed responses as errors: $response",
async ({ response, description }) => {
const { result, request } = setup("remove");
request.mockResolvedValue(response);
await act(async () => {
await result.current.run();
});
expect(toast).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
title: "Failed to remove package",
description,
variant: "danger",
}),
);
},
);
it("reports request errors and allows a retry", async () => {
const { result, request } = setup("remove");
request.mockRejectedValueOnce(new Error("offline"));
await act(async () => {
await result.current.run();
});
expect(toast).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
title: "Failed to remove package",
description: "offline",
variant: "danger",
}),
);
expect(result.current.loading).toBe(false);
request.mockResolvedValue({ success: true });
await act(async () => {
await result.current.run();
});
expect(toast).toHaveBeenLastCalledWith(
expect.objectContaining({ title: "Package removed" }),
);
});
});