test: 添加完整测试套件(52 个文件,326 个用例)
基于 Vitest 搭建测试基础设施,覆盖后端纯函数、API 路由、 前端 hooks、UI 组件和页面级集成测试。
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import AuthModal from "./AuthModal";
|
||||
|
||||
vi.mock("@/lib/userId", () => ({
|
||||
setCachedProfile: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const mockOnAuth = vi.fn();
|
||||
const mockOnClose = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset localStorage
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: { setItem: vi.fn(), getItem: vi.fn(), removeItem: vi.fn() },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
function renderModal(props = {}) {
|
||||
return render(
|
||||
<AuthModal open onClose={mockOnClose} onAuth={mockOnAuth} {...props} />,
|
||||
);
|
||||
}
|
||||
|
||||
describe("AuthModal", () => {
|
||||
it("renders login tab by default", () => {
|
||||
renderModal();
|
||||
expect(screen.getAllByText("登录").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("注册").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByPlaceholderText("请输入用户名")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders register tab when specified", () => {
|
||||
renderModal({ defaultTab: "register" });
|
||||
expect(screen.getByPlaceholderText("2-16 个字符")).toBeInTheDocument();
|
||||
expect(screen.getByText("确认密码")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches between login and register tabs", async () => {
|
||||
renderModal();
|
||||
expect(screen.getByPlaceholderText("请输入用户名")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("注册"));
|
||||
expect(screen.getByPlaceholderText("2-16 个字符")).toBeInTheDocument();
|
||||
expect(screen.getByText("确认密码")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows error when username is empty", async () => {
|
||||
renderModal();
|
||||
const submitBtn = screen.getAllByText("登录").find(
|
||||
(el) => el.closest("button[class*='bg-accent']"),
|
||||
)!;
|
||||
fireEvent.click(submitBtn);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("请输入用户名")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error when password is empty", async () => {
|
||||
renderModal();
|
||||
const user = userEvent.setup();
|
||||
await user.type(screen.getByPlaceholderText("请输入用户名"), "testuser");
|
||||
const submitBtn = screen.getAllByText("登录").find(
|
||||
(el) => el.closest("button[class*='bg-accent']"),
|
||||
)!;
|
||||
fireEvent.click(submitBtn);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("请输入密码")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows register-specific validation errors", async () => {
|
||||
renderModal({ defaultTab: "register" });
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(screen.getByPlaceholderText("2-16 个字符"), "a");
|
||||
await user.type(screen.getByPlaceholderText("至少 6 个字符"), "123456");
|
||||
await user.type(screen.getByPlaceholderText("再次输入密码"), "654321");
|
||||
|
||||
const submitBtn = screen.getAllByText("注册").find(
|
||||
(el) => el.closest("button[class*='bg-accent']"),
|
||||
)!;
|
||||
fireEvent.click(submitBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("用户名需要 2-16 个字符")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows password mismatch error during registration", async () => {
|
||||
renderModal({ defaultTab: "register" });
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(screen.getByPlaceholderText("2-16 个字符"), "testuser");
|
||||
await user.type(screen.getByPlaceholderText("至少 6 个字符"), "123456");
|
||||
await user.type(screen.getByPlaceholderText("再次输入密码"), "654321");
|
||||
|
||||
const submitBtn = screen.getAllByText("注册").find(
|
||||
(el) => el.closest("button[class*='bg-accent']"),
|
||||
)!;
|
||||
fireEvent.click(submitBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("两次密码不一致")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("submits login successfully", async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: "u1", username: "testuser", avatar: "🐱" }),
|
||||
});
|
||||
|
||||
renderModal();
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(screen.getByPlaceholderText("请输入用户名"), "testuser");
|
||||
await user.type(screen.getByPlaceholderText("请输入密码"), "password123");
|
||||
|
||||
const submitBtn = screen.getAllByText("登录").find(
|
||||
(el) => el.closest("button[class*='bg-accent']"),
|
||||
)!;
|
||||
fireEvent.click(submitBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnAuth).toHaveBeenCalledWith({
|
||||
id: "u1",
|
||||
username: "testuser",
|
||||
avatar: "🐱",
|
||||
});
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows API error on failed login", async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ error: "用户名或密码错误" }),
|
||||
});
|
||||
|
||||
renderModal();
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(screen.getByPlaceholderText("请输入用户名"), "testuser");
|
||||
await user.type(screen.getByPlaceholderText("请输入密码"), "wrong");
|
||||
|
||||
const submitBtn = screen.getAllByText("登录").find(
|
||||
(el) => el.closest("button[class*='bg-accent']"),
|
||||
)!;
|
||||
fireEvent.click(submitBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("用户名或密码错误")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows network error on fetch failure", async () => {
|
||||
mockFetch.mockRejectedValue(new Error("network"));
|
||||
|
||||
renderModal();
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(screen.getByPlaceholderText("请输入用户名"), "testuser");
|
||||
await user.type(screen.getByPlaceholderText("请输入密码"), "password123");
|
||||
|
||||
const submitBtn = screen.getAllByText("登录").find(
|
||||
(el) => el.closest("button[class*='bg-accent']"),
|
||||
)!;
|
||||
fireEvent.click(submitBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("网络错误,请重试")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles password visibility", () => {
|
||||
renderModal();
|
||||
const toggle = screen.getByLabelText("显示密码");
|
||||
const passwordInput = screen.getByPlaceholderText("请输入密码");
|
||||
|
||||
expect(passwordInput).toHaveAttribute("type", "password");
|
||||
fireEvent.click(toggle);
|
||||
expect(passwordInput).toHaveAttribute("type", "text");
|
||||
});
|
||||
|
||||
it("does not render when closed", () => {
|
||||
render(
|
||||
<AuthModal open={false} onClose={mockOnClose} onAuth={mockOnAuth} />,
|
||||
);
|
||||
expect(screen.queryByText("欢迎")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders avatar selection in register mode", () => {
|
||||
renderModal({ defaultTab: "register" });
|
||||
expect(screen.getByText("选择头像")).toBeInTheDocument();
|
||||
expect(screen.getByText("🐱")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import BlindboxDrawnHistory, { type DrawnIdea } from "./BlindboxDrawnHistory";
|
||||
|
||||
const mockItems: DrawnIdea[] = [
|
||||
{
|
||||
id: "drawn-1",
|
||||
content: "去公园野餐",
|
||||
createdAt: "2025-03-01T10:00:00Z",
|
||||
user: { id: "u1", username: "小明", avatar: "🐱" },
|
||||
drawnBy: { id: "u2", username: "小红", avatar: "🐶" },
|
||||
},
|
||||
{
|
||||
id: "drawn-2",
|
||||
content: "看展览",
|
||||
createdAt: "2025-03-02T10:00:00Z",
|
||||
user: { id: "u2", username: "小红", avatar: "🐶" },
|
||||
drawnBy: null,
|
||||
},
|
||||
];
|
||||
|
||||
describe("BlindboxDrawnHistory", () => {
|
||||
it("renders nothing when items are empty", () => {
|
||||
const { container } = render(<BlindboxDrawnHistory items={[]} />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("renders history title and items", () => {
|
||||
render(<BlindboxDrawnHistory items={mockItems} />);
|
||||
expect(screen.getByText("履约记录")).toBeInTheDocument();
|
||||
expect(screen.getByText("去公园野餐")).toBeInTheDocument();
|
||||
expect(screen.getByText("看展览")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows user who contributed and who drew", () => {
|
||||
render(<BlindboxDrawnHistory items={mockItems} />);
|
||||
expect(screen.getByText(/小明 投入/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/小红 抽中/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders formatted date", () => {
|
||||
render(<BlindboxDrawnHistory items={mockItems} />);
|
||||
const dateElements = screen.getAllByText(/月/);
|
||||
expect(dateElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import BlindboxMyIdeas, { type MyIdea, CategoryBadge, DurationLabel } from "./BlindboxMyIdeas";
|
||||
|
||||
const mockIdeas: MyIdea[] = [
|
||||
{
|
||||
id: "idea-1",
|
||||
content: "去爬山",
|
||||
createdAt: new Date().toISOString(),
|
||||
category: "outdoor",
|
||||
estimatedMinutes: 120,
|
||||
},
|
||||
{
|
||||
id: "idea-2",
|
||||
content: "看电影",
|
||||
createdAt: new Date().toISOString(),
|
||||
category: "entertainment",
|
||||
estimatedMinutes: 150,
|
||||
},
|
||||
];
|
||||
|
||||
const mockOnEdit = vi.fn().mockResolvedValue(undefined);
|
||||
const mockOnDelete = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
describe("BlindboxMyIdeas", () => {
|
||||
it("renders idea list with count", () => {
|
||||
render(
|
||||
<BlindboxMyIdeas ideas={mockIdeas} onEdit={mockOnEdit} onDelete={mockOnDelete} />,
|
||||
);
|
||||
expect(screen.getByText(/我投入的想法(2)/)).toBeInTheDocument();
|
||||
expect(screen.getByText("去爬山")).toBeInTheDocument();
|
||||
expect(screen.getByText("看电影")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows duration labels", () => {
|
||||
render(
|
||||
<BlindboxMyIdeas ideas={mockIdeas} onEdit={mockOnEdit} onDelete={mockOnDelete} />,
|
||||
);
|
||||
expect(screen.getByText("~2h")).toBeInTheDocument();
|
||||
expect(screen.getByText("~2.5h")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders empty list", () => {
|
||||
render(
|
||||
<BlindboxMyIdeas ideas={[]} onEdit={mockOnEdit} onDelete={mockOnDelete} />,
|
||||
);
|
||||
expect(screen.getByText(/我投入的想法(0)/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enters edit mode on pencil button click", async () => {
|
||||
render(
|
||||
<BlindboxMyIdeas ideas={[mockIdeas[0]]} onEdit={mockOnEdit} onDelete={mockOnDelete} />,
|
||||
);
|
||||
const editButtons = screen.getAllByRole("button");
|
||||
const pencilBtn = editButtons.find((b) => b.querySelector("svg"));
|
||||
fireEvent.click(pencilBtn!);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CategoryBadge", () => {
|
||||
it("renders icon for known category", () => {
|
||||
const { container } = render(<CategoryBadge category="dining" />);
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders fallback for unknown category", () => {
|
||||
const { container } = render(<CategoryBadge category="unknown" />);
|
||||
expect(container.textContent).toBe("💡");
|
||||
});
|
||||
|
||||
it("renders fallback for null category", () => {
|
||||
const { container } = render(<CategoryBadge category={null} />);
|
||||
expect(container.textContent).toBe("💡");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DurationLabel", () => {
|
||||
it("renders minutes", () => {
|
||||
render(<DurationLabel minutes={45} />);
|
||||
expect(screen.getByText("~45min")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders hours", () => {
|
||||
render(<DurationLabel minutes={60} />);
|
||||
expect(screen.getByText("~1h")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders nothing for null", () => {
|
||||
const { container } = render(<DurationLabel minutes={null} />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import BlindboxPlan from "./BlindboxPlan";
|
||||
import type { WeekendPlanData } from "@/types";
|
||||
|
||||
beforeEach(() => {
|
||||
Element.prototype.scrollTo = vi.fn();
|
||||
});
|
||||
|
||||
vi.mock("@/components/BlindboxMyIdeas", () => ({
|
||||
CategoryBadge: ({ category }: { category: string }) => (
|
||||
<span data-testid="category-badge">{category}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockDays: WeekendPlanData[] = [
|
||||
{
|
||||
date: "周六",
|
||||
summary: "轻松的一天",
|
||||
items: [
|
||||
{
|
||||
time: "10:00",
|
||||
activity: "去公园散步",
|
||||
poi: "朝阳公园",
|
||||
address: "朝阳区",
|
||||
duration: 120,
|
||||
lat: 39.9,
|
||||
lng: 116.4,
|
||||
reason: "空气好",
|
||||
},
|
||||
{
|
||||
time: "14:00",
|
||||
activity: "午餐",
|
||||
poi: "海底捞",
|
||||
address: "朝阳区",
|
||||
duration: 90,
|
||||
lat: 39.9,
|
||||
lng: 116.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "周日",
|
||||
summary: "文艺的一天",
|
||||
items: [
|
||||
{
|
||||
time: "10:00",
|
||||
activity: "参观博物馆",
|
||||
poi: "国家博物馆",
|
||||
address: "东城区",
|
||||
duration: 180,
|
||||
lat: 39.9,
|
||||
lng: 116.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockOnAccept = vi.fn();
|
||||
const mockOnRegenerate = vi.fn();
|
||||
const mockOnShare = vi.fn();
|
||||
const mockOnBack = vi.fn();
|
||||
|
||||
function renderPlan(overrides = {}) {
|
||||
return render(
|
||||
<BlindboxPlan
|
||||
days={mockDays}
|
||||
onAccept={mockOnAccept}
|
||||
onRegenerate={mockOnRegenerate}
|
||||
onShare={mockOnShare}
|
||||
onBack={mockOnBack}
|
||||
{...overrides}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("BlindboxPlan", () => {
|
||||
it("renders day header with date", () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText(/周六 · 行程规划/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders summary text", () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText("轻松的一天")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders timeline items", () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText("去公园散步")).toBeInTheDocument();
|
||||
expect(screen.getByText("朝阳公园")).toBeInTheDocument();
|
||||
expect(screen.getByText("午餐")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows duration formatted", () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText("2h")).toBeInTheDocument();
|
||||
expect(screen.getByText("1h30min")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows accept and regenerate buttons when not accepted", () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText("接受契约")).toBeInTheDocument();
|
||||
expect(screen.getByText("换一个方案")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows share button when accepted", () => {
|
||||
renderPlan({ accepted: true });
|
||||
expect(screen.getByText("分享计划")).toBeInTheDocument();
|
||||
expect(screen.queryByText("接受契约")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onAccept", () => {
|
||||
renderPlan();
|
||||
fireEvent.click(screen.getByText("接受契约"));
|
||||
expect(mockOnAccept).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onRegenerate", () => {
|
||||
renderPlan();
|
||||
fireEvent.click(screen.getByText("换一个方案"));
|
||||
expect(mockOnRegenerate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onBack", () => {
|
||||
renderPlan();
|
||||
fireEvent.click(screen.getByText("返回想法池"));
|
||||
expect(mockOnBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("navigates between days", async () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText(/周六 · 行程规划/)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("周日"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/周日 · 行程规划/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows day indicators for multi-day plans", () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText("1 / 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows navigation link for items with coordinates", () => {
|
||||
renderPlan();
|
||||
const navLinks = screen.getAllByText("导航");
|
||||
expect(navLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("shows reason when available", () => {
|
||||
renderPlan();
|
||||
expect(screen.getByText("空气好")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Button from "./Button";
|
||||
|
||||
describe("Button", () => {
|
||||
it("renders children text", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByText("Click me")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with primary variant by default", () => {
|
||||
render(<Button>Primary</Button>);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button.className).toContain("bg-accent");
|
||||
});
|
||||
|
||||
it("renders secondary variant", () => {
|
||||
render(<Button variant="secondary">Secondary</Button>);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button.className).toContain("bg-surface");
|
||||
});
|
||||
|
||||
it("renders danger variant", () => {
|
||||
render(<Button variant="danger">Danger</Button>);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button.className).toContain("bg-rose-600");
|
||||
});
|
||||
|
||||
it("shows loading spinner and disables button", () => {
|
||||
render(<Button loading>Loading</Button>);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toBeDisabled();
|
||||
expect(button.querySelector(".animate-spin")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows loadingText when loading", () => {
|
||||
render(
|
||||
<Button loading loadingText="请稍候...">
|
||||
Submit
|
||||
</Button>,
|
||||
);
|
||||
expect(screen.getByText("请稍候...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables button when disabled prop is set", () => {
|
||||
render(<Button disabled>Disabled</Button>);
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("applies full width class", () => {
|
||||
render(<Button fullWidth>Full Width</Button>);
|
||||
expect(screen.getByRole("button").className).toContain("w-full");
|
||||
});
|
||||
|
||||
it("applies pill shape", () => {
|
||||
render(<Button shape="pill">Pill</Button>);
|
||||
expect(screen.getByRole("button").className).toContain("rounded-full");
|
||||
});
|
||||
|
||||
it("applies size classes", () => {
|
||||
const { rerender } = render(<Button size="sm">Small</Button>);
|
||||
expect(screen.getByRole("button").className).toContain("h-8");
|
||||
|
||||
rerender(<Button size="lg">Large</Button>);
|
||||
expect(screen.getByRole("button").className).toContain("h-11");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { Package } from "lucide-react";
|
||||
import EmptyState from "./EmptyState";
|
||||
|
||||
describe("EmptyState", () => {
|
||||
it("renders title", () => {
|
||||
render(<EmptyState icon={Package} title="没有数据" />);
|
||||
expect(screen.getByText("没有数据")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders subtitle when provided", () => {
|
||||
render(
|
||||
<EmptyState icon={Package} title="空" subtitle="还没有任何内容" />,
|
||||
);
|
||||
expect(screen.getByText("还没有任何内容")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render subtitle when not provided", () => {
|
||||
const { container } = render(<EmptyState icon={Package} title="空" />);
|
||||
expect(container.querySelectorAll("p")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders CTA button when ctaLabel and onCta provided", () => {
|
||||
const onCta = vi.fn();
|
||||
render(
|
||||
<EmptyState icon={Package} title="空" ctaLabel="添加" onCta={onCta} />,
|
||||
);
|
||||
const button = screen.getByText("添加");
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(button);
|
||||
expect(onCta).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not render CTA without both ctaLabel and onCta", () => {
|
||||
render(<EmptyState icon={Package} title="空" ctaLabel="添加" />);
|
||||
expect(screen.queryByText("添加")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders image when provided", () => {
|
||||
render(
|
||||
<EmptyState icon={Package} title="空" image="/test-image.png" />,
|
||||
);
|
||||
expect(screen.getByAltText("空")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import Input from "./Input";
|
||||
|
||||
describe("Input", () => {
|
||||
it("renders an input element", () => {
|
||||
render(<Input placeholder="Enter text" />);
|
||||
expect(screen.getByPlaceholderText("Enter text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("accepts user input", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Input placeholder="Type here" />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Type here");
|
||||
await user.type(input, "hello");
|
||||
expect(input).toHaveValue("hello");
|
||||
});
|
||||
|
||||
it("applies size variants", () => {
|
||||
const { rerender } = render(<Input size="sm" data-testid="input" />);
|
||||
expect(screen.getByTestId("input").className).toContain("h-8");
|
||||
|
||||
rerender(<Input size="lg" data-testid="input" />);
|
||||
expect(screen.getByTestId("input").className).toContain("h-10");
|
||||
|
||||
rerender(<Input size="xl" data-testid="input" />);
|
||||
expect(screen.getByTestId("input").className).toContain("h-11");
|
||||
});
|
||||
|
||||
it("applies purple variant", () => {
|
||||
render(<Input variant="purple" data-testid="input" />);
|
||||
expect(screen.getByTestId("input").className).toContain("focus:ring-purple-600");
|
||||
});
|
||||
|
||||
it("forwards ref", () => {
|
||||
const ref = { current: null as HTMLInputElement | null };
|
||||
render(<Input ref={ref} />);
|
||||
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
||||
});
|
||||
|
||||
it("supports disabled state", () => {
|
||||
render(<Input disabled data-testid="input" />);
|
||||
expect(screen.getByTestId("input")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { TEST_RESTAURANT, TEST_RESTAURANT_2 } from "@/__tests__/helpers/fixtures";
|
||||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import { ToastContext, type ToastContextValue } from "@/hooks/useToast";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/celebrate", () => ({
|
||||
fireCelebration: vi.fn(),
|
||||
playChime: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/userId", () => ({
|
||||
isRegistered: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
vi.mock("./ShareCardModal", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("./RestaurantImage", () => ({
|
||||
default: ({ alt }: { alt: string }) => <img alt={alt} />,
|
||||
}));
|
||||
|
||||
vi.mock("./AuthModal", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("./NoMatchResult", () => ({
|
||||
default: ({ onReset }: { onReset: () => void }) => (
|
||||
<div data-testid="no-match">
|
||||
<button onClick={onReset}>重来</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./RunnerUpCard", () => ({
|
||||
default: ({ restaurant }: { restaurant: { name: string } }) => (
|
||||
<div data-testid="runner-up">{restaurant.name}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({}) });
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
import MatchResult from "./MatchResult";
|
||||
|
||||
const toastCtx: ToastContextValue = { show: vi.fn() };
|
||||
|
||||
function Wrapper({ children }: { children: ReactNode }) {
|
||||
return React.createElement(
|
||||
ToastContext.Provider,
|
||||
{ value: toastCtx },
|
||||
children,
|
||||
);
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
restaurant: TEST_RESTAURANT,
|
||||
matchLikes: 2,
|
||||
runnerUps: [],
|
||||
allRestaurants: [TEST_RESTAURANT, TEST_RESTAURANT_2],
|
||||
userCount: 2,
|
||||
roomId: "ROOM01",
|
||||
userId: "user-1",
|
||||
onReset: vi.fn().mockResolvedValue(undefined),
|
||||
onNarrow: vi.fn().mockResolvedValue(undefined),
|
||||
resetting: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("MatchResult", () => {
|
||||
it("renders unanimous match display", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="unanimous" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("就去这了")).toBeInTheDocument();
|
||||
expect(screen.getByText("大家一拍即合!")).toBeInTheDocument();
|
||||
expect(screen.getByText("测试餐厅")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders best match display", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="best" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("就去这了")).toBeInTheDocument();
|
||||
expect(screen.getByText("2/2 人想去这家")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders no_match by delegating to NoMatchResult", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="no_match" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByTestId("no-match")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows navigation button", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="unanimous" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("导航过去")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows phone button when tel available", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="unanimous" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("打电话订位")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows share button", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="best" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("生成分享卡片")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows solo message when userCount is 1", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="unanimous" userCount={1} />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("帮你选好了")).toBeInTheDocument();
|
||||
expect(screen.getByText("你的首选,别犹豫了")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows reset button", () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<MatchResult {...baseProps} matchType="unanimous" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("再来一轮")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import Modal from "./Modal";
|
||||
|
||||
describe("Modal", () => {
|
||||
it("renders children when open", () => {
|
||||
render(
|
||||
<Modal open onClose={() => {}}>
|
||||
<p>Modal content</p>
|
||||
</Modal>,
|
||||
);
|
||||
expect(screen.getByText("Modal content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when closed", () => {
|
||||
render(
|
||||
<Modal open={false} onClose={() => {}}>
|
||||
<p>Hidden content</p>
|
||||
</Modal>,
|
||||
);
|
||||
expect(screen.queryByText("Hidden content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClose when clicking backdrop", () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<Modal open onClose={onClose}>
|
||||
<p>Content</p>
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const backdrop = screen.getByText("Content").closest("[class*='fixed']");
|
||||
if (backdrop) {
|
||||
fireEvent.click(backdrop);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not close when clicking content", () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<Modal open onClose={onClose}>
|
||||
<p>Content</p>
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Content"));
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies sheet variant by default", () => {
|
||||
render(
|
||||
<Modal open onClose={() => {}}>
|
||||
<p>Sheet</p>
|
||||
</Modal>,
|
||||
);
|
||||
const content = screen.getByText("Sheet").closest("div[class*='rounded']");
|
||||
expect(content?.className).toContain("rounded-t-3xl");
|
||||
});
|
||||
|
||||
it("applies dialog variant", () => {
|
||||
render(
|
||||
<Modal open onClose={() => {}} variant="dialog">
|
||||
<p>Dialog</p>
|
||||
</Modal>,
|
||||
);
|
||||
const content = screen.getByText("Dialog").closest("div[class*='rounded']");
|
||||
expect(content?.className).toContain("rounded-2xl");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import {
|
||||
Skeleton,
|
||||
SkeletonCircle,
|
||||
RoomCardSkeleton,
|
||||
ProfileCardSkeleton,
|
||||
RecordItemSkeleton,
|
||||
SwipeDeckSkeleton,
|
||||
BlindboxRoomSkeleton,
|
||||
BlindboxListSkeleton,
|
||||
} from "./Skeleton";
|
||||
|
||||
describe("Skeleton", () => {
|
||||
it("renders with animate-pulse", () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
expect(container.firstChild).toHaveClass("animate-pulse");
|
||||
});
|
||||
|
||||
it("accepts custom className", () => {
|
||||
const { container } = render(<Skeleton className="h-4 w-20" />);
|
||||
const el = container.firstChild as HTMLElement;
|
||||
expect(el.className).toContain("h-4");
|
||||
expect(el.className).toContain("w-20");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkeletonCircle", () => {
|
||||
it("renders rounded-full shape", () => {
|
||||
const { container } = render(<SkeletonCircle />);
|
||||
expect(container.firstChild).toHaveClass("rounded-full");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Skeleton composites", () => {
|
||||
it("renders RoomCardSkeleton", () => {
|
||||
const { container } = render(<RoomCardSkeleton />);
|
||||
expect(container.querySelectorAll(".animate-pulse").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders ProfileCardSkeleton", () => {
|
||||
const { container } = render(<ProfileCardSkeleton />);
|
||||
expect(container.querySelectorAll(".animate-pulse").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders RecordItemSkeleton", () => {
|
||||
const { container } = render(<RecordItemSkeleton />);
|
||||
expect(container.querySelectorAll(".animate-pulse").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders SwipeDeckSkeleton", () => {
|
||||
const { container } = render(<SwipeDeckSkeleton />);
|
||||
expect(container.querySelectorAll(".animate-pulse").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders BlindboxRoomSkeleton", () => {
|
||||
const { container } = render(<BlindboxRoomSkeleton />);
|
||||
expect(container.querySelectorAll(".animate-pulse").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders BlindboxListSkeleton", () => {
|
||||
const { container } = render(<BlindboxListSkeleton />);
|
||||
expect(container.querySelectorAll(".animate-pulse").length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import SwipeDeck from "./SwipeDeck";
|
||||
import {
|
||||
TEST_RESTAURANT,
|
||||
TEST_RESTAURANT_2,
|
||||
TEST_RESTAURANT_3,
|
||||
} from "@/__tests__/helpers/fixtures";
|
||||
|
||||
vi.mock("./SwipeableCard", () => ({
|
||||
default: ({ restaurant }: { restaurant: { name: string } }) => (
|
||||
<div data-testid="swipeable-card">{restaurant.name}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./ActionButtons", () => ({
|
||||
default: ({ disabled }: { disabled: boolean }) => (
|
||||
<div data-testid="action-buttons" data-disabled={disabled} />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./MatchResult", () => ({
|
||||
default: ({ restaurant }: { restaurant: { name: string } }) => (
|
||||
<div data-testid="match-result">{restaurant.name}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./SwipeGuide", () => ({
|
||||
default: () => <div data-testid="swipe-guide" />,
|
||||
}));
|
||||
|
||||
vi.mock("./UserAvatar", () => ({
|
||||
default: () => <span data-testid="user-avatar" />,
|
||||
}));
|
||||
|
||||
const restaurants = [TEST_RESTAURANT, TEST_RESTAURANT_2, TEST_RESTAURANT_3];
|
||||
|
||||
const defaultProps = {
|
||||
restaurants,
|
||||
roomId: "ROOM01",
|
||||
userId: "user-1",
|
||||
initialIndex: 0,
|
||||
matchedRestaurantId: null,
|
||||
matchType: null as const,
|
||||
matchLikes: 0,
|
||||
runnerUps: [],
|
||||
likeCounts: {},
|
||||
swipeCounts: {},
|
||||
userCount: 2,
|
||||
userProfiles: {},
|
||||
onReset: vi.fn().mockResolvedValue(undefined),
|
||||
onNarrow: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
describe("SwipeDeck", () => {
|
||||
it("renders restaurant cards at initial index", () => {
|
||||
render(<SwipeDeck {...defaultProps} />);
|
||||
const cards = screen.getAllByTestId("swipeable-card");
|
||||
expect(cards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders action buttons", () => {
|
||||
render(<SwipeDeck {...defaultProps} />);
|
||||
expect(screen.getByTestId("action-buttons")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows swipe guide at index 0", () => {
|
||||
render(<SwipeDeck {...defaultProps} />);
|
||||
expect(screen.getByTestId("swipe-guide")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show swipe guide when initialIndex > 0", () => {
|
||||
render(<SwipeDeck {...defaultProps} initialIndex={1} />);
|
||||
expect(screen.queryByTestId("swipe-guide")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows match result when matchedRestaurantId is set", () => {
|
||||
render(
|
||||
<SwipeDeck
|
||||
{...defaultProps}
|
||||
matchedRestaurantId={TEST_RESTAURANT.id}
|
||||
matchType="unanimous"
|
||||
matchLikes={2}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("match-result")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables action buttons when match exists", () => {
|
||||
render(
|
||||
<SwipeDeck
|
||||
{...defaultProps}
|
||||
matchedRestaurantId={TEST_RESTAURANT.id}
|
||||
matchType="unanimous"
|
||||
/>,
|
||||
);
|
||||
const actions = screen.getByTestId("action-buttons");
|
||||
expect(actions.getAttribute("data-disabled")).toBe("true");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import SwipeableCard from "./SwipeableCard";
|
||||
import { TEST_RESTAURANT } from "@/__tests__/helpers/fixtures";
|
||||
|
||||
vi.mock("./RestaurantCard", () => ({
|
||||
default: ({ restaurant }: { restaurant: { name: string } }) => (
|
||||
<div data-testid="restaurant-card">{restaurant.name}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("SwipeableCard", () => {
|
||||
it("renders restaurant card", () => {
|
||||
render(
|
||||
<SwipeableCard
|
||||
restaurant={TEST_RESTAURANT}
|
||||
isTop
|
||||
onSwipe={() => {}}
|
||||
likeCount={0}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("restaurant-card")).toBeInTheDocument();
|
||||
expect(screen.getByText("测试餐厅")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders swipe overlays (LIKE and NOPE)", () => {
|
||||
const { container } = render(
|
||||
<SwipeableCard
|
||||
restaurant={TEST_RESTAURANT}
|
||||
isTop
|
||||
onSwipe={() => {}}
|
||||
likeCount={0}
|
||||
/>,
|
||||
);
|
||||
expect(container.textContent).toContain("LIKE");
|
||||
expect(container.textContent).toContain("NOPE");
|
||||
});
|
||||
|
||||
it("calls registerSwipe when provided and isTop", () => {
|
||||
const registerSwipe = vi.fn();
|
||||
render(
|
||||
<SwipeableCard
|
||||
restaurant={TEST_RESTAURANT}
|
||||
isTop
|
||||
onSwipe={() => {}}
|
||||
registerSwipe={registerSwipe}
|
||||
likeCount={0}
|
||||
/>,
|
||||
);
|
||||
expect(registerSwipe).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it("does not pass registerSwipe for non-top cards", () => {
|
||||
render(
|
||||
<SwipeableCard
|
||||
restaurant={TEST_RESTAURANT}
|
||||
isTop={false}
|
||||
onSwipe={() => {}}
|
||||
likeCount={0}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("restaurant-card")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays like count", () => {
|
||||
render(
|
||||
<SwipeableCard
|
||||
restaurant={TEST_RESTAURANT}
|
||||
isTop
|
||||
onSwipe={() => {}}
|
||||
likeCount={3}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("restaurant-card")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Toast from "./Toast";
|
||||
|
||||
describe("Toast", () => {
|
||||
it("renders message when provided", () => {
|
||||
render(<Toast message="操作成功" />);
|
||||
expect(screen.getByText("操作成功")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when message is empty", () => {
|
||||
const { container } = render(<Toast message="" />);
|
||||
expect(container.textContent).toBe("");
|
||||
});
|
||||
|
||||
it("applies top position class by default", () => {
|
||||
render(<Toast message="test" />);
|
||||
const el = screen.getByText("test").closest("div[class*='fixed']");
|
||||
expect(el?.className).toContain("top-10");
|
||||
});
|
||||
|
||||
it("applies bottom position class", () => {
|
||||
render(<Toast message="test" position="bottom" />);
|
||||
const el = screen.getByText("test").closest("div[class*='fixed']");
|
||||
expect(el?.className).toContain("bottom-8");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import TopNav from "./TopNav";
|
||||
|
||||
vi.mock("./QrInviteModal", () => ({
|
||||
default: ({ open }: { open: boolean }) =>
|
||||
open ? <div data-testid="qr-modal">QR Modal</div> : null,
|
||||
}));
|
||||
|
||||
vi.mock("./RoomManageModal", () => ({
|
||||
default: ({ open }: { open: boolean }) =>
|
||||
open ? <div data-testid="manage-modal">Manage Modal</div> : null,
|
||||
}));
|
||||
|
||||
describe("TopNav", () => {
|
||||
it("renders room ID in invite button", () => {
|
||||
render(<TopNav roomId="ROOM01" />);
|
||||
expect(screen.getByText(/ROOM01/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders exit button", () => {
|
||||
const onExit = vi.fn();
|
||||
render(<TopNav roomId="ROOM01" onExit={onExit} />);
|
||||
|
||||
const exitBtn = screen.getByLabelText("退出房间");
|
||||
fireEvent.click(exitBtn);
|
||||
expect(onExit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows manage button only for creator", () => {
|
||||
const { rerender } = render(<TopNav roomId="ROOM01" isCreator={false} />);
|
||||
expect(screen.queryByText("管理")).not.toBeInTheDocument();
|
||||
|
||||
rerender(<TopNav roomId="ROOM01" isCreator />);
|
||||
expect(screen.getByText("管理")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens QR invite modal on button click", () => {
|
||||
render(<TopNav roomId="ROOM01" />);
|
||||
expect(screen.queryByTestId("qr-modal")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText(/ROOM01/));
|
||||
expect(screen.getByTestId("qr-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens manage modal on manage button click", () => {
|
||||
render(<TopNav roomId="ROOM01" isCreator />);
|
||||
expect(screen.queryByTestId("manage-modal")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("管理"));
|
||||
expect(screen.getByTestId("manage-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders brand title", () => {
|
||||
render(<TopNav roomId="ROOM01" />);
|
||||
expect(screen.getByText("NoWhatever")).toBeInTheDocument();
|
||||
expect(screen.getByText("别说随便")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import WeekendTimeSelector from "./WeekendTimeSelector";
|
||||
|
||||
const mockOnConfirm = vi.fn();
|
||||
const mockOnClose = vi.fn();
|
||||
|
||||
function renderSelector(props = {}) {
|
||||
return render(
|
||||
<WeekendTimeSelector
|
||||
onConfirm={mockOnConfirm}
|
||||
onClose={mockOnClose}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("WeekendTimeSelector", () => {
|
||||
it("renders time presets", () => {
|
||||
renderSelector();
|
||||
expect(screen.getByText("周六全天")).toBeInTheDocument();
|
||||
expect(screen.getByText("周日全天")).toBeInTheDocument();
|
||||
expect(screen.getByText("整个周末")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders hour selectors", () => {
|
||||
renderSelector();
|
||||
const selects = screen.getAllByRole("combobox");
|
||||
expect(selects).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("shows confirm button", () => {
|
||||
renderSelector();
|
||||
expect(screen.getByText("生成周末计划")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onConfirm with default config", () => {
|
||||
renderSelector();
|
||||
fireEvent.click(screen.getByText("生成周末计划"));
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith({
|
||||
date: "周六",
|
||||
startHour: 10,
|
||||
endHour: 21,
|
||||
});
|
||||
});
|
||||
|
||||
it("calls onClose when close button clicked", () => {
|
||||
renderSelector();
|
||||
fireEvent.click(screen.getByLabelText("关闭"));
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("switches preset on click", () => {
|
||||
renderSelector();
|
||||
fireEvent.click(screen.getByText("周日全天"));
|
||||
fireEvent.click(screen.getByText("生成周末计划"));
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith({
|
||||
date: "周日",
|
||||
startHour: 10,
|
||||
endHour: 21,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user