diff --git a/components/doctor/detailDoctor/List.js b/components/doctor/detailDoctor/List.js index bc3251f..0cc7750 100644 --- a/components/doctor/detailDoctor/List.js +++ b/components/doctor/detailDoctor/List.js @@ -15,6 +15,9 @@ import jsPDF from "jspdf"; import { Button } from "@mui/material"; import { toast } from "react-toastify"; +const A4_WIDTH_MM = 210; +const A4_HEIGHT_MM = 297; + const VAZIR_WEIGHTS = [ [400, "Vazirmatn-RD-FD-Regular.ttf"], [500, "Vazirmatn-RD-FD-Medium.ttf"], @@ -99,17 +102,27 @@ function List({ hiddenRef, data, qrReady }) { pxHeight = canvas.height; } - const imgWidth = 210; - const pxPerMm = pxWidth / imgWidth; - const imgHeight = pxHeight / pxPerMm; + // کاغذ همیشه A4 است، نه اندازه‌ای که از نسبت تصویر درمی‌آید: خروجی قبلی + // ۲۱۰×۲۶۲٫۵ میلی‌متر بود و روی هیچ پرینتری A4 حساب نمی‌شد. + const pdf = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); - const pdf = new jsPDF({ - orientation: "portrait", - unit: "mm", - format: [imgWidth, imgHeight], - }); + // پوستر خودش نسبت A4 دارد، پس عملاً کل صفحه را پر می‌کند. این محاسبه تور + // ایمنی است تا اگر ابعاد پوستر روزی عوض شد، تصویر از کاغذ بیرون نزند. + let width = A4_WIDTH_MM; + let height = (pxHeight / pxWidth) * width; + if (height > A4_HEIGHT_MM) { + height = A4_HEIGHT_MM; + width = (pxWidth / pxHeight) * height; + } - pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight); + pdf.addImage( + imgData, + "PNG", + (A4_WIDTH_MM - width) / 2, + (A4_HEIGHT_MM - height) / 2, + width, + height, + ); const safeName = (data?.name ?? "doctor").replace(/[\\/:*?"<>|]+/g, "-"); pdf.save(`poster-${safeName}.pdf`); } catch { diff --git a/components/doctor/detailDoctor/Share.js b/components/doctor/detailDoctor/Share.js index 36df54f..fded38a 100644 --- a/components/doctor/detailDoctor/Share.js +++ b/components/doctor/detailDoctor/Share.js @@ -41,7 +41,7 @@ function Share({ data }) {

اشتراک گذاری

-
+
{variant === "dark" ? ( diff --git a/components/doctor/detailDoctor/posterPdf.test.js b/components/doctor/detailDoctor/posterPdf.test.js new file mode 100644 index 0000000..d4681e2 --- /dev/null +++ b/components/doctor/detailDoctor/posterPdf.test.js @@ -0,0 +1,94 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { fireEvent } from "@testing-library/dom"; + +// خروجی قبلی کاغذِ [۲۱۰ × ارتفاعِ تصویر] می‌ساخت، یعنی ۲۱۰×۲۶۲٫۵ میلی‌متر. روی هیچ +// پرینتری A4 حساب نمی‌شد. این تست همان قرارداد را قفل می‌کند: یک صفحهٔ A4. +const jsPdfCalls = { ctor: [], addImage: [], save: [] }; + +vi.mock("jspdf", () => ({ + default: class { + constructor(options) { + jsPdfCalls.ctor.push(options); + } + addImage(...args) { + jsPdfCalls.addImage.push(args); + } + save(name) { + jsPdfCalls.save.push(name); + } + }, +})); + +vi.mock("html-to-image", () => ({ + toPng: vi.fn(async () => "data:image/png;base64,AAAA"), +})); + +vi.mock("html2canvas", () => ({ default: vi.fn() })); + +vi.mock("react-toastify", () => ({ + toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() }, +})); + +// فونت وزیر با fetch به data-url تبدیل می‌شود؛ در jsdom باید تأمین شود وگرنه مسیر +// جایگزینِ html2canvas اجرا می‌شود و تست چیز دیگری را می‌سنجد. +vi.stubGlobal("fetch", vi.fn(async () => ({ blob: async () => new Blob(["x"]) }))); + +import List from "./List"; + +// پوستر A4 است: ۱۰۸۰×۱۵۲۷ فیزیکی، که با pixelRatio=2 دو برابر می‌شود. +function posterRef({ width = 1080, height = 1527 } = {}) { + const element = document.createElement("div"); + Object.defineProperty(element, "offsetWidth", { value: width }); + Object.defineProperty(element, "offsetHeight", { value: height }); + element.querySelectorAll = () => []; + return { current: element }; +} + +async function download(ref) { + render(); + fireEvent.click(screen.getByLabelText("دانلود")); + await waitFor(() => expect(jsPdfCalls.save.length).toBe(1)); +} + +describe("PDF پوستر", () => { + beforeEach(() => { + jsPdfCalls.ctor = []; + jsPdfCalls.addImage = []; + jsPdfCalls.save = []; + }); + + it("کاغذ A4 است، نه اندازه‌ای که از نسبت تصویر درمی‌آید", async () => { + await download(posterRef()); + + expect(jsPdfCalls.ctor[0]).toMatchObject({ + orientation: "portrait", + unit: "mm", + format: "a4", + }); + }); + + it("پوستر A4 کل صفحه را پر می‌کند و یک صفحه بیشتر نیست", async () => { + await download(posterRef()); + + const [, , x, y, width, height] = jsPdfCalls.addImage[0]; + + expect(jsPdfCalls.addImage).toHaveLength(1); + expect(width).toBeCloseTo(210, 1); + expect(height).toBeCloseTo(297, 0); + expect(x).toBeCloseTo(0, 1); + expect(y).toBeCloseTo(0, 0); + }); + + /** تور ایمنی: اگر ابعاد پوستر روزی بلندتر شد، تصویر باید کوچک شود نه اینکه از کاغذ بزند بیرون. */ + it("پوستر بلندتر از A4 داخل کاغذ جا داده می‌شود، نه بریده", async () => { + await download(posterRef({ width: 1080, height: 2000 })); + + const [, , x, y, width, height] = jsPdfCalls.addImage[0]; + + expect(height).toBeLessThanOrEqual(297); + expect(width).toBeLessThanOrEqual(210); + expect(x).toBeGreaterThan(0); + expect(y).toBeCloseTo(0, 1); + }); +}); diff --git a/components/doctor/poster/PosterLight.js b/components/doctor/poster/PosterLight.js index 455a13c..034a85d 100644 --- a/components/doctor/poster/PosterLight.js +++ b/components/doctor/poster/PosterLight.js @@ -47,7 +47,7 @@ function PosterLight({ data, onQrReady }) { return (
{/* top brand band */}
-
+ {/* بالا-چین می‌ماند تا آواتار روی نوار بالایی سوار بماند؛ فاصلهٔ اضافیِ کاغذ A4 + به‌صورت فضای خالی بالای فوتر می‌نشیند، نه بین آواتار و نوار. */} +
{/* avatar overlapping band */}
@@ -176,9 +176,11 @@ function Poster({ data, onQrReady }) { {/* body */} -
+ {/* کارت‌ها به هم چسبیده می‌مانند و فاصلهٔ اضافیِ کاغذ A4 بالای فوتر می‌نشیند: + حفرهٔ وسطِ صفحه شبیه محتوای جاافتاده دیده می‌شد. */} +
{shownServices.length > 0 && ( -
+
}>خدمات
{shownServices.map((s, i) => ( diff --git a/components/doctor/poster/poster.test.js b/components/doctor/poster/poster.test.js index d6c4b94..b1e0222 100644 --- a/components/doctor/poster/poster.test.js +++ b/components/doctor/poster/poster.test.js @@ -49,13 +49,16 @@ describe.each([ expect(screen.getByText(/تخصص دیگر/)).toBeInTheDocument(); }); - it("کادر ثابت ۱۰۸۰×۱۳۵۰ و overflow-hidden دست‌نخورده است", () => { + // ۱۰۸۰×۱۵۲۷ همان نسبت A4 است (۲۱۰:۲۹۷). هر عدد دیگری یعنی PDF یا حاشیهٔ سفید + // می‌گیرد یا از کاغذ بیرون می‌زند. + it("کادر ثابت ۱۰۸۰×۱۵۲۷ با نسبت A4 و overflow-hidden دست‌نخورده است", () => { const { container } = render(); const frame = container.firstChild; expect(frame.className).toContain("w-[1080px]"); - expect(frame.className).toContain("h-[1350px]"); + expect(frame.className).toContain("h-[1527px]"); expect(frame.className).toContain("overflow-hidden"); + expect(1527 / 1080).toBeCloseTo(297 / 210, 2); }); it("خط زیرتخصص از بودجهٔ کاراکتر نمی‌گذرد", () => {