- ایجاد پرامپت برای رفع خطای 404 ناشی از لینک `/doctor/undefined` در سایتهای بهبهان و یاسوج. - افزودن کامپوننت `CityHighlights` برای نمایش محتوای یکتا و آمار پزشکان و تخصصهای پرمراجعه در صفحهٔ اصلی هر شهر. - بهروزرسانی تستهای مربوط به کامپوننتهای `ItemDoctor` و `CityHighlights` برای اطمینان از عدم وجود لینکهای نامعتبر و نمایش صحیح اطلاعات. - ایجاد سند جدید برای مستندسازی خطاهای Search Console که باگ نیستند و نیاز به رفع ندارند. - افزودن تستهای واحد برای توابع `buildCityIntro` و دیگر توابع مرتبط با تولید متن یکتا برای صفحات لیست.
208 lines
7.2 KiB
JavaScript
208 lines
7.2 KiB
JavaScript
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);
|
|
});
|
|
});
|