eCourt-note/tests/utils/pdf-export.test.ts
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 eddcec28c7 Add application codebase: PDF annotation for e-Court
Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight,
freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo,
shapes, redaction, export PDF/CSV, reply, resolve, search, summary,
textbox, stamp, version history)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 14:29:40 +07:00

150 lines
6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import {
displayRectToPdf,
displaySize,
displayToPdfPoint,
encodeRFC5987,
hexToRgb01,
wrapStyledText,
wrapTextByWidth,
} from "@/utils/pdf-export";
// หน้า A4 portrait จริง (pt) — display ของ 90/270 จะสลับเป็น 792×612
const PAGE = { width: 612, height: 792 };
describe("displaySize", () => {
it("/Rotate 0 และ 180 → ขนาดหน้าเดิม", () => {
expect(displaySize(PAGE, 0)).toEqual(PAGE);
expect(displaySize(PAGE, 180)).toEqual(PAGE);
});
it("/Rotate 90 และ 270 → สลับกว้าง-สูง", () => {
expect(displaySize(PAGE, 90)).toEqual({ width: 792, height: 612 });
expect(displaySize(PAGE, 270)).toEqual({ width: 792, height: 612 });
});
});
describe("displayToPdfPoint", () => {
it("rot 0: พลิกแกน y (จอ y ลง → PDF y ขึ้น)", () => {
expect(displayToPdfPoint({ x: 10, y: 20 }, PAGE, 0)).toEqual({ x: 10, y: 772 });
});
it("rot 90: จอมุมบนซ้าย (0,0) → PDF (0,0)", () => {
expect(displayToPdfPoint({ x: 0, y: 0 }, PAGE, 90)).toEqual({ x: 0, y: 0 });
expect(displayToPdfPoint({ x: 10, y: 20 }, PAGE, 90)).toEqual({ x: 20, y: 10 });
});
it("rot 180: มุมบนซ้ายจอ → มุมขวาล่างของ PDF", () => {
expect(displayToPdfPoint({ x: 10, y: 20 }, PAGE, 180)).toEqual({ x: 602, y: 20 });
});
it("rot 270: มุมบนซ้ายจอ → มุมขวาบนของ PDF", () => {
expect(displayToPdfPoint({ x: 10, y: 20 }, PAGE, 270)).toEqual({ x: 592, y: 782 });
});
});
describe("displayRectToPdf", () => {
it("rot 0: y พลิก (y เดิม = ขอบบนจอ → กลายเป็น y บนของ rect PDF)", () => {
expect(displayRectToPdf({ x: 10, y: 20, width: 30, height: 40 }, PAGE, 0)).toEqual({
x: 10,
y: 732,
width: 30,
height: 40,
});
});
it("rot 90: กว้าง-สูงสลับกัน และ normalized เป็นบวกเสมอ", () => {
expect(displayRectToPdf({ x: 10, y: 20, width: 30, height: 40 }, PAGE, 90)).toEqual({
x: 20,
y: 10,
width: 40,
height: 30,
});
});
});
describe("hexToRgb01", () => {
it("แปลง #rrggbb เป็นสัดส่วน 0–1", () => {
expect(hexToRgb01("#ff0000")).toEqual({ r: 1, g: 0, b: 0 });
expect(hexToRgb01("#00ff00")).toEqual({ g: 1, r: 0, b: 0 });
expect(hexToRgb01("#0000ff")).toEqual({ r: 0, g: 0, b: 1 });
});
it("ไม่มี # นำหน้า / ตัวพิมพ์ใหญ่ก็รับ", () => {
expect(hexToRgb01("ff8000")).toEqual({ r: 1, g: 128 / 255, b: 0 });
expect(hexToRgb01("#FF8000")).toEqual({ r: 1, g: 128 / 255, b: 0 });
});
it("รูปแบบผิด → ดำ (fallback กัน parse พัง)", () => {
expect(hexToRgb01("abc")).toEqual({ r: 0, g: 0, b: 0 });
expect(hexToRgb01("#zzzzzz")).toEqual({ r: 0, g: 0, b: 0 });
expect(hexToRgb01("")).toEqual({ r: 0, g: 0, b: 0 });
});
});
describe("wrapTextByWidth", () => {
// จำลองฟอนต์ที่ตัวอักษรกว้าง 1 หน่วยทุกตัว
const w = (s: string) => s.length;
it("ตัดทีละตัวอักษร (ภาษาไทยไม่มีช่องว่างคำ)", () => {
expect(wrapTextByWidth("อบจทดสอบ", 5, w)).toEqual(["อบจทด", "สอบ"]);
});
it("ขึ้นบรรทัดใหม่ด้วย \\n แยกจากการ wrap", () => {
expect(wrapTextByWidth("aaaa\nbb", 5, w)).toEqual(["aaaa", "bb"]);
});
it("คำยาวเกิน maxWidth → แตกต่อทันทีไม่มีการกลืน", () => {
expect(wrapTextByWidth("x".repeat(12), 5, w)).toEqual(["xxxxx", "xxxxx", "xx"]);
});
it("ข้อความว่าง → บรรทัดว่างเดียว", () => {
expect(wrapTextByWidth("", 5, w)).toEqual([""]);
});
});
describe("wrapStyledText", () => {
// ตัวหนากว้าง 2 เท่าของปกติ
const w = (s: string, bold: boolean) => s.length * (bold ? 2 : 1);
it("run ผสมหนา/ปกติ wrap ตามความกว้างจริงของแต่ละตัว แล้วม้วนกลับเป็น run ต่อบรรทัด", () => {
const out = wrapStyledText(
[
{ text: "ab", bold: true },
{ text: "cdef", bold: false },
],
6,
w,
);
// a(2)+b(2)+c(1)+d(1) = 6 พอดีบรรทัดแรก · e,f ไปบรรทัดสอง
expect(out).toEqual([
[
{ text: "ab", bold: true },
{ text: "cd", bold: false },
],
[{ text: "ef", bold: false }],
]);
});
it("\\n ใน run ตัดบรรทัดแบบชัด ๆ", () => {
const out = wrapStyledText([{ text: "a\nb", bold: false }], 10, w);
expect(out).toEqual([[{ text: "a", bold: false }], [{ text: "b", bold: false }]]);
});
it("ตัวอักษรที่กว้างเกิน maxWidth ทั้งบรรทัด ยังวางลงบรรทัดใหม่ได้ (ไม่ทิ้ง)", () => {
const out = wrapStyledText([{ text: "ab", bold: true }], 1, w);
expect(out).toEqual([[{ text: "a", bold: true }], [{ text: "b", bold: true }]]);
});
});
describe("encodeRFC5987", () => {
it("ชื่อไฟล์ภาษาไทย encode เป็น percent-encoding UTF-8", () => {
expect(encodeRFC5987("คำพิพากษา.pdf")).toBe(
"%E0%B8%84%E0%B8%B3%E0%B8%9E%E0%B8%B4%E0%B8%9E%E0%B8%B2%E0%B8%81%E0%B8%A9%E0%B8%B2.pdf",
);
});
it("อักขระ '()* ที่ encodeURIComponent ปล่อยไว้ ต้องถูก encode เพิ่มตาม RFC 5987", () => {
expect(encodeRFC5987("a'b(c)*d")).toBe("a%27b%28c%29%2Ad");
});
});