import { describe, expect, it, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; const getStateInfo = vi.fn(); const fetchReq = vi.fn(); vi.mock("@/lib/getStateInfo", () => ({ getStateInfo: (...a) => getStateInfo(...a) })); vi.mock("@/lib/req", () => ({ fetchReq: (...a) => fetchReq(...a) })); const { default: CityHighlights } = await import("@/components/home/CityHighlights"); const BEHBAHAN = { id: 41, name: "بهبهان", site_name: "بهبهان نوبت" }; const counts = (rows) => ({ data: { data: rows } }); const ROWS = [ { id: 1, name: "دندانپزشک", slug: "dentistry", number_of_doctors: 12 }, { id: 2, name: "داخلی", slug: "internal-medicine", number_of_doctors: 7 }, { id: 3, name: "بدون اسلاگ", slug: null, number_of_doctors: 9 }, { id: 4, name: "بدون پزشک", slug: "empty", number_of_doctors: 0 }, ]; const DOCTORS = { data: [ { uuid: "u-1", name: "معصومه خدری", owner_status: "claimed", specialties: [{ id: "1", name: "داخلی" }], address: [{ id: 1 }], }, { uuid: "u-2", name: "پروفایل بی‌مالک", owner_status: "unclaimed", specialties: [{ id: "1", name: "داخلی" }], }, ], }; const renderComponent = async () => render(await CityHighlights()); /** مسیرِ فراخوانی را از روی URL تشخیص می‌دهد؛ ترتیب Promise.all نباید تست را بشکند. */ const routeMock = ({ countsBody = counts(ROWS), doctorsBody = DOCTORS } = {}) => fetchReq.mockImplementation((url) => Promise.resolve(url.includes("doctor-counts") ? countsBody : doctorsBody) ); beforeEach(() => { getStateInfo.mockReset(); fetchReq.mockReset(); getStateInfo.mockResolvedValue({ matchedCity: BEHBAHAN, isRoot: false }); routeMock(); }); describe("CityHighlights", () => { it("متن یکتا و نام شهر را نشان می‌دهد", async () => { await renderComponent(); expect(screen.getAllByText(/بهبهان/).length).toBeGreaterThan(0); expect( screen.getByText("تخصص‌های پرمراجعه در بهبهان") ).toBeInTheDocument(); }); it("تخصص‌ها را از پرپزشک به کم‌پزشک می‌چیند و لینک می‌دهد", async () => { await renderComponent(); const links = screen.getAllByRole("link"); expect(links[0]).toHaveAttribute("href", "/specialties/dentistry"); expect(links[1]).toHaveAttribute("href", "/specialties/internal-medicine"); }); it("تخصص بدون slug یا بدون پزشک نمایش داده نمی‌شود", async () => { await renderComponent(); expect(screen.queryByText("بدون اسلاگ")).not.toBeInTheDocument(); expect(screen.queryByText("بدون پزشک")).not.toBeInTheDocument(); }); it("شمار کل پزشکان در متن می‌آید", async () => { await renderComponent(); // فقط ردیف‌های معتبر: 12 + 7 = 19 expect(screen.getByText(/19/)).toBeInTheDocument(); }); // ── پزشکان شاخص ─────────────────────────────────────────────────────────── it("پزشکان همان شهر را با لینک مستقیم نشان می‌دهد", async () => { await renderComponent(); expect(screen.getByText("پزشکان بهبهان")).toBeInTheDocument(); expect(screen.getByRole("link", { name: /معصومه خدری/ })).toHaveAttribute( "href", "/doctor/u-1" ); }); it("پروفایل noindex لینک نمی‌گیرد", async () => { // همان سیاست sitemap؛ لینک از صفحهٔ اصلی به صفحهٔ ایندکس‌نشده سیگنال متناقض است. await renderComponent(); expect(screen.queryByText("پروفایل بی‌مالک")).not.toBeInTheDocument(); }); it("شهرِ بدون پزشکِ ایندکس‌پذیر، بخش پزشکان را حذف می‌کند ولی تخصص‌ها می‌مانند", async () => { routeMock({ doctorsBody: { data: [] } }); await renderComponent(); expect(screen.queryByText(/^پزشکان /)).not.toBeInTheDocument(); expect(screen.getByText("تخصص‌های پرمراجعه در بهبهان")).toBeInTheDocument(); }); it("قطعی سرویس پزشکان بقیهٔ بخش را نمی‌شکند", async () => { routeMock({ doctorsBody: null }); await renderComponent(); expect(screen.getByText("تخصص‌های پرمراجعه در بهبهان")).toBeInTheDocument(); }); it("حداکثر چهار پزشک نشان می‌دهد", async () => { routeMock({ doctorsBody: { data: Array.from({ length: 10 }, (_, i) => ({ uuid: `d-${i}`, name: `پزشک ${i}`, owner_status: "claimed", specialties: [{ id: "1", name: "داخلی" }], })), }, }); await renderComponent(); const doctorLinks = screen .getAllByRole("link") .filter((a) => a.getAttribute("href").startsWith("/doctor/")); expect(doctorLinks).toHaveLength(4); }); // ── مسیرهایی که باید چیزی رندر نکنند ────────────────────────────────────── it("دامنهٔ ریشه هیچ بخشی نمی‌سازد", async () => { getStateInfo.mockResolvedValue({ matchedCity: { id: 600 }, isRoot: true }); const { container } = await renderComponent(); expect(container).toBeEmptyDOMElement(); expect(fetchReq).not.toHaveBeenCalled(); }); it("قطعی API صفحه را نمی‌شکند", async () => { // fetchReq در خطا خودش null می‌دهد؛ صفحهٔ اصلی نباید سفید شود. fetchReq.mockResolvedValue(null); const { container } = await renderComponent(); expect(container).toBeEmptyDOMElement(); }); it("پاسخ با شکل غیرمنتظره هم کرش نمی‌کند", async () => { fetchReq.mockResolvedValue({ data: { data: "not-an-array" } }); const { container } = await renderComponent(); expect(container).toBeEmptyDOMElement(); }); it("شهرِ بدون پزشک بخش را حذف می‌کند، نه اینکه صفر نشان دهد", async () => { fetchReq.mockResolvedValue( counts([{ id: 9, name: "x", slug: "x", number_of_doctors: 0 }]) ); const { container } = await renderComponent(); expect(container).toBeEmptyDOMElement(); expect(container.textContent).not.toContain("0 پزشک"); }); it("شهرِ بدون شناسه بخش را حذف می‌کند", async () => { getStateInfo.mockResolvedValue({ matchedCity: null, isRoot: false }); const { container } = await renderComponent(); expect(container).toBeEmptyDOMElement(); }); it("حداکثر هشت تخصص نشان می‌دهد", async () => { fetchReq.mockResolvedValue( counts( Array.from({ length: 20 }, (_, i) => ({ id: i, name: `تخصص ${i}`, slug: `s-${i}`, number_of_doctors: 20 - i, })) ) ); await renderComponent(); expect(screen.getAllByRole("link")).toHaveLength(8); }); });