/**
* ModelCard 互動測試(Phase 0.9 模型下載)
*
* 覆蓋:
* - 下載按鈕顯示條件:converted + ready 顯示;uploaded / 非 ready 不顯示
* - 點下載按鈕:preventDefault + stopPropagation(不觸發外層 導航)
* - 下載中:按鈕 disabled + 顯示「下載中」
* - 成功 → toast.success;失敗 → toast.error(用 backend code 對應 i18n)
* - 其他卡片下載中時,本卡按鈕 disabled
*
* Mock:
* - sonner toast → 攔截 success / error
* - model-store downloadModel action → vi.spyOn 控制回傳
* - next/navigation(jsdom 無 app router context)
*/
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
import { LocaleProvider } from "@/lib/i18n/context";
import { useModelStore, type ModelSummary } from "@/stores/model-store";
import { ModelCard } from "./model-card";
vi.mock("sonner", () => {
const success = vi.fn();
const error = vi.fn();
return { toast: Object.assign(vi.fn(), { success, error }) };
});
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
prefetch: vi.fn(),
}),
}));
import { toast } from "sonner";
const convertedReady: ModelSummary = {
id: "m1",
name: "YOLOv5s",
targetChip: "kl520",
fileSize: 1024,
source: "converted",
status: "ready",
createdAt: "2026-01-01T00:00:00Z",
};
function renderCard(model: ModelSummary) {
return render(
,
);
}
beforeEach(() => {
useModelStore.setState({ downloadingId: null });
(toast.success as Mock).mockReset();
(toast.error as Mock).mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
useModelStore.setState({ downloadingId: null });
});
describe("ModelCard 下載按鈕顯示條件", () => {
it("converted + ready → 顯示下載按鈕", () => {
renderCard(convertedReady);
expect(screen.getByTestId("model-card-download")).toBeInTheDocument();
});
it("uploaded(即使 ready)→ 不顯示下載按鈕", () => {
renderCard({ ...convertedReady, source: "uploaded" });
expect(screen.queryByTestId("model-card-download")).not.toBeInTheDocument();
});
it("converted 但 scanning → 不顯示下載按鈕", () => {
renderCard({ ...convertedReady, status: "scanning" });
expect(screen.queryByTestId("model-card-download")).not.toBeInTheDocument();
});
it("preset → 不顯示下載按鈕", () => {
renderCard({ ...convertedReady, source: "preset" });
expect(screen.queryByTestId("model-card-download")).not.toBeInTheDocument();
});
});
describe("ModelCard 下載互動", () => {
it("點下載按鈕:preventDefault + stopPropagation(不觸發 Link 導航)", async () => {
const spy = vi
.spyOn(useModelStore.getState(), "downloadModel")
.mockResolvedValue({ ok: true });
renderCard(convertedReady);
const btn = screen.getByTestId("model-card-download");
// 自建 event 驗 preventDefault / stopPropagation 都被呼叫
const event = new MouseEvent("click", { bubbles: true, cancelable: true });
const preventDefault = vi.spyOn(event, "preventDefault");
const stopPropagation = vi.spyOn(event, "stopPropagation");
fireEvent(btn, event);
expect(preventDefault).toHaveBeenCalled();
expect(stopPropagation).toHaveBeenCalled();
await waitFor(() => expect(spy).toHaveBeenCalledWith(convertedReady));
});
it("下載成功 → toast.success", async () => {
vi.spyOn(useModelStore.getState(), "downloadModel").mockResolvedValue({ ok: true });
renderCard(convertedReady);
fireEvent.click(screen.getByTestId("model-card-download"));
await waitFor(() => expect(toast.success).toHaveBeenCalledOnce());
expect(toast.error).not.toHaveBeenCalled();
});
it("下載失敗(forbidden)→ toast.error,描述用對應 i18n", async () => {
vi.spyOn(useModelStore.getState(), "downloadModel").mockResolvedValue({
ok: false,
code: "forbidden",
message: "no",
});
renderCard(convertedReady);
fireEvent.click(screen.getByTestId("model-card-download"));
await waitFor(() => expect(toast.error).toHaveBeenCalledOnce());
// 第二參數 description 對應 models.download.error.forbidden(繁中)
const call = (toast.error as Mock).mock.calls[0];
expect(call[1].description).toBe("你沒有權限下載此模型");
});
it("未知 code → toast.error 描述退化成 unknown 文案", async () => {
vi.spyOn(useModelStore.getState(), "downloadModel").mockResolvedValue({
ok: false,
code: "some_unmapped_code",
message: "x",
});
renderCard(convertedReady);
fireEvent.click(screen.getByTestId("model-card-download"));
await waitFor(() => expect(toast.error).toHaveBeenCalledOnce());
const call = (toast.error as Mock).mock.calls[0];
expect(call[1].description).toBe("下載失敗,請稍後再試");
});
it("其他卡片下載中(downloadingId != null)→ 本卡按鈕 disabled", () => {
useModelStore.setState({ downloadingId: "other-model" });
renderCard(convertedReady);
expect(screen.getByTestId("model-card-download")).toBeDisabled();
});
it("本卡下載中 → 顯示「下載中…」且 disabled", () => {
useModelStore.setState({ downloadingId: "m1" });
renderCard(convertedReady);
const btn = screen.getByTestId("model-card-download");
expect(btn).toBeDisabled();
expect(btn).toHaveTextContent("下載中");
});
});