feat: Update poster dimensions to A4 size and enhance PDF generation tests
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -41,7 +41,7 @@ function Share({ data }) {
|
||||
<ShareDI />
|
||||
<p className="hidden lg:flex">اشتراک گذاری</p>
|
||||
</Button>
|
||||
<div className="fixed w-[1080px] h-[1350px] top-0 left-0 opacity-0 pointer-events-none -z-10">
|
||||
<div className="fixed w-[1080px] h-[1527px] top-0 left-0 opacity-0 pointer-events-none -z-10">
|
||||
<div ref={hiddenRef}>
|
||||
{variant === "dark" ? (
|
||||
<Poster data={data} onQrReady={handleQrReady} />
|
||||
|
||||
@@ -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(<List hiddenRef={ref} data={{ name: "مرضیه حسینی" }} qrReady />);
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -47,7 +47,7 @@ function PosterLight({ data, onQrReady }) {
|
||||
return (
|
||||
<div
|
||||
dir="rtl"
|
||||
className="relative w-[1080px] h-[1350px] overflow-hidden flex flex-col bg-[#F6F9FD] text-center"
|
||||
className="relative w-[1080px] h-[1527px] overflow-hidden flex flex-col bg-[#F6F9FD] text-center"
|
||||
>
|
||||
{/* top brand band */}
|
||||
<div
|
||||
@@ -71,7 +71,9 @@ function PosterLight({ data, onQrReady }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-[72px] flex flex-col items-center">
|
||||
{/* بالا-چین میماند تا آواتار روی نوار بالایی سوار بماند؛ فاصلهٔ اضافیِ کاغذ A4
|
||||
بهصورت فضای خالی بالای فوتر مینشیند، نه بین آواتار و نوار. */}
|
||||
<div className="flex-1 px-[72px] pb-[40px] flex flex-col items-center">
|
||||
{/* avatar overlapping band */}
|
||||
<div className="-mt-[8px] pt-[40px]">
|
||||
<div
|
||||
|
||||
@@ -84,7 +84,7 @@ function Poster({ data, onQrReady }) {
|
||||
return (
|
||||
<div
|
||||
dir="rtl"
|
||||
className="relative w-[1080px] h-[1350px] overflow-hidden flex flex-col p-[64px] text-right"
|
||||
className="relative w-[1080px] h-[1527px] overflow-hidden flex flex-col p-[64px] text-right"
|
||||
style={{ background: POSTER_BG }}
|
||||
>
|
||||
<div className="pointer-events-none absolute -top-[180px] -left-[180px] w-[520px] h-[520px] rounded-full bg-[#57A6FF]/20 blur-[80px]" />
|
||||
@@ -176,9 +176,11 @@ function Poster({ data, onQrReady }) {
|
||||
</section>
|
||||
|
||||
{/* body */}
|
||||
<section className="relative mt-[36px] flex-1">
|
||||
{/* کارتها به هم چسبیده میمانند و فاصلهٔ اضافیِ کاغذ A4 بالای فوتر مینشیند:
|
||||
حفرهٔ وسطِ صفحه شبیه محتوای جاافتاده دیده میشد. */}
|
||||
<section className="relative mt-[36px] flex-1 flex flex-col gap-[28px]">
|
||||
{shownServices.length > 0 && (
|
||||
<div className="rounded-[28px] bg-white/6 border border-white/10 p-[32px] mb-[28px]">
|
||||
<div className="rounded-[28px] bg-white/6 border border-white/10 p-[32px]">
|
||||
<SectionTitle icon={<ClipBoard />}>خدمات</SectionTitle>
|
||||
<div className="flex flex-wrap items-center gap-x-[12px] gap-y-[14px]">
|
||||
{shownServices.map((s, i) => (
|
||||
|
||||
@@ -49,13 +49,16 @@ describe.each([
|
||||
expect(screen.getByText(/تخصص دیگر/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("کادر ثابت ۱۰۸۰×۱۳۵۰ و overflow-hidden دستنخورده است", () => {
|
||||
// ۱۰۸۰×۱۵۲۷ همان نسبت A4 است (۲۱۰:۲۹۷). هر عدد دیگری یعنی PDF یا حاشیهٔ سفید
|
||||
// میگیرد یا از کاغذ بیرون میزند.
|
||||
it("کادر ثابت ۱۰۸۰×۱۵۲۷ با نسبت A4 و overflow-hidden دستنخورده است", () => {
|
||||
const { container } = render(<Component data={JAHANTAB} />);
|
||||
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("خط زیرتخصص از بودجهٔ کاراکتر نمیگذرد", () => {
|
||||
|
||||
Reference in New Issue
Block a user