test: add frontend test suite
Some checks failed
Deploy to Frontend Servers / deploy (push) Failing after 43s
Some checks failed
Deploy to Frontend Servers / deploy (push) Failing after 43s
This commit is contained in:
100
src/components/ResourceCard.test.tsx
Normal file
100
src/components/ResourceCard.test.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { Resource } from "../api";
|
||||
import { I18nProvider } from "../i18n";
|
||||
import { ResourceCard } from "./ResourceCard";
|
||||
|
||||
const resource: Resource = {
|
||||
id: "resource-1",
|
||||
title: "Demo Resource",
|
||||
description: "Short description",
|
||||
type: "pdf",
|
||||
language: "zh-TW",
|
||||
categoryId: 1,
|
||||
categorySlug: "docs",
|
||||
categoryName: "文件",
|
||||
coverImage: "/uploads/cover.png",
|
||||
fileUrl: "/uploads/demo.pdf",
|
||||
previewUrl: "/uploads/preview.pdf",
|
||||
badgeLabel: "官方",
|
||||
isDownloadable: true,
|
||||
isRecommended: true,
|
||||
updatedAt: "2026-05-16T00:00:00Z",
|
||||
tags: ["ark"],
|
||||
};
|
||||
|
||||
function renderCard(onFavoriteToggle = vi.fn()) {
|
||||
return {
|
||||
user: userEvent.setup(),
|
||||
onFavoriteToggle,
|
||||
...render(
|
||||
<MemoryRouter>
|
||||
<I18nProvider>
|
||||
<ResourceCard r={resource} onFavoriteToggle={onFavoriteToggle} />
|
||||
</I18nProvider>
|
||||
</MemoryRouter>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function okResponse() {
|
||||
return new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
describe("ResourceCard", () => {
|
||||
it("renders resource summary and detail link", () => {
|
||||
renderCard();
|
||||
|
||||
expect(screen.getByText("Demo Resource")).toBeInTheDocument();
|
||||
expect(screen.getByText("文件")).toBeInTheDocument();
|
||||
expect(screen.getByText("官方")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: /預覽/ })).toHaveAttribute(
|
||||
"href",
|
||||
"/resource/resource-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("toggles local favorite state and syncs best-effort API delta", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(okResponse());
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const { user, onFavoriteToggle } = renderCard();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /收藏/ }));
|
||||
|
||||
expect(JSON.parse(localStorage.getItem("ark_favorites") || "[]")).toEqual([
|
||||
"resource-1",
|
||||
]);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/resources/resource-1/favorite",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ add: true }),
|
||||
}),
|
||||
);
|
||||
expect(onFavoriteToggle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("tracks downloads then opens the downloadable file", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(okResponse());
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const openMock = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
const { user } = renderCard();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /下載/ }));
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/resources/resource-1/download",
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
expect(openMock).toHaveBeenCalledWith(
|
||||
"/uploads/demo.pdf",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
});
|
||||
});
|
||||
52
src/components/ResourceListFooter.test.tsx
Normal file
52
src/components/ResourceListFooter.test.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ResourceListFooter } from "./ResourceListFooter";
|
||||
|
||||
const t = (key: string) =>
|
||||
({
|
||||
listRange: "顯示 {{from}}–{{to}},共 {{total}} 筆",
|
||||
paginationPrev: "上一頁",
|
||||
paginationNext: "下一頁",
|
||||
pageIndicator: "{{c}} / {{p}} 頁",
|
||||
})[key] ?? key;
|
||||
|
||||
describe("ResourceListFooter", () => {
|
||||
it("renders range, page count, and disabled prev on first page", () => {
|
||||
render(
|
||||
<ResourceListFooter
|
||||
page={1}
|
||||
limit={24}
|
||||
total={50}
|
||||
t={t}
|
||||
onPrev={vi.fn()}
|
||||
onNext={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("顯示 1–24,共 50 筆")).toBeInTheDocument();
|
||||
expect(screen.getByText("1 / 3 頁")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "上一頁" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "下一頁" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("calls navigation handlers when buttons are enabled", () => {
|
||||
const onPrev = vi.fn();
|
||||
const onNext = vi.fn();
|
||||
render(
|
||||
<ResourceListFooter
|
||||
page={2}
|
||||
limit={24}
|
||||
total={50}
|
||||
t={t}
|
||||
onPrev={onPrev}
|
||||
onNext={onNext}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "上一頁" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "下一頁" }));
|
||||
|
||||
expect(onPrev).toHaveBeenCalledTimes(1);
|
||||
expect(onNext).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user