import { describe, expect, it } from "vitest"; import { clamp, getPointerPercent, mergeRectsByLine, pathToPercent, pathToPx, pointToPercent, pointToPx, rectToPercent, rectToPx, toPercent, toPx, type Size, } from "@/utils/coordinates"; const size: Size = { width: 800, height: 600 }; describe("toPercent / toPx", () => { it("px → % ตามสัดส่วนขนาดที่ส่งเข้ามา", () => { expect(toPercent(400, 800)).toBe(50); expect(toPercent(300, 600)).toBe(50); expect(toPercent(0, 800)).toBe(0); }); it("roundtrip px → % → px ได้ค่าเดิม", () => { expect(toPx(toPercent(123, 800), 800)).toBeCloseTo(123); }); }); describe("clamp", () => { it("ต่ำกว่า min → min, อยู่ในช่วง → ค่าเดิม, เกิน max → max", () => { expect(clamp(-5, 0, 100)).toBe(0); expect(clamp(42, 0, 100)).toBe(42); expect(clamp(120, 0, 100)).toBe(100); }); }); describe("point/rect/path converters", () => { it("point: x อิง width, y อิง height (แกนแยกกัน)", () => { expect(pointToPercent({ x: 400, y: 300 }, size)).toEqual({ x: 50, y: 50 }); expect(pointToPx({ x: 25, y: 75 }, size)).toEqual({ x: 200, y: 450 }); }); it("rect: แปลงครบทั้ง 4 ค่า", () => { expect(rectToPercent({ x: 80, y: 60, width: 160, height: 120 }, size)).toEqual({ x: 10, y: 10, width: 20, height: 20, }); expect(rectToPx({ x: 10, y: 10, width: 20, height: 20 }, size)).toEqual({ x: 80, y: 60, width: 160, height: 120, }); }); it("path: แปลงทุกจุดในลิสต์", () => { expect( pathToPercent( [ { x: 0, y: 0 }, { x: 800, y: 600 }, ], size, ), ).toEqual([ { x: 0, y: 0 }, { x: 100, y: 100 }, ]); expect(pathToPx([{ x: 50, y: 50 }], size)).toEqual([{ x: 400, y: 300 }]); }); }); describe("getPointerPercent", () => { const makeEl = (left: number, top: number, width: number, height: number) => ({ getBoundingClientRect: () => ({ left, top, width, height }) }) as HTMLElement; it("คิดจากขอบ element ไม่ใช่ขอบจอ", () => { const el = makeEl(100, 50, 200, 100); expect(getPointerPercent({ clientX: 200, clientY: 100 }, el)).toEqual({ x: 50, y: 50 }); }); it("เกินขอบ element → clamp 100 / ต่ำกว่าขอบ → clamp 0", () => { const el = makeEl(100, 50, 200, 100); expect(getPointerPercent({ clientX: 999, clientY: 999 }, el)).toEqual({ x: 100, y: 100 }); expect(getPointerPercent({ clientX: 0, clientY: 0 }, el)).toEqual({ x: 0, y: 0 }); }); }); describe("mergeRectsByLine", () => { it("rects บรรทัดเดียวกัน (y overlap) → รวมเป็น 1 rect ครอบกว้าง/สูงสุด", () => { const out = mergeRectsByLine([ { x: 10, y: 20, width: 30, height: 5 }, { x: 45, y: 21, width: 20, height: 5 }, ]); expect(out).toHaveLength(1); expect(out[0]).toEqual({ x: 10, y: 20, width: 55, height: 6 }); }); it("rects คนละบรรทัด (y ไม่ overlap) → คงแยกกัน", () => { const out = mergeRectsByLine([ { x: 10, y: 20, width: 30, height: 5 }, { x: 12, y: 30, width: 30, height: 5 }, ]); expect(out).toHaveLength(2); }); it("บรรทัดติดกันที่กล่องซ้อนกันเล็กน้อย (leading แน่น/ฟอนต์ไทย) → ยังแยกบรรทัดกัน", () => { // ซ้อน 1 หน่วยจากความสูง 5 (20%) — ไม่ใช่บรรทัดเดียวกัน const out = mergeRectsByLine([ { x: 10, y: 20, width: 30, height: 5 }, { x: 12, y: 24, width: 30, height: 5 }, ]); expect(out).toHaveLength(2); }); it("ลำดับ input ไม่สำคัญ — rect ซ้ายมาทีหลังก็รวมถูกจุดเริ่ม", () => { const out = mergeRectsByLine([ { x: 45, y: 21, width: 20, height: 5 }, { x: 10, y: 20, width: 30, height: 5 }, ]); expect(out).toHaveLength(1); expect(out[0]).toMatchObject({ x: 10, width: 55 }); }); it("ช่องว่างระหว่างคำในบรรทัดเดียวกันถูกครอบด้วย (union แกน x)", () => { const out = mergeRectsByLine([ { x: 10, y: 20, width: 10, height: 5 }, { x: 80, y: 20, width: 10, height: 5 }, ]); expect(out).toHaveLength(1); expect(out[0].width).toBe(80); // 10 → 90 }); it("array ว่าง → ว่าง / rect เดี่ยว → ค่าเดิม", () => { expect(mergeRectsByLine([])).toEqual([]); expect(mergeRectsByLine([{ x: 1, y: 2, width: 3, height: 4 }])).toEqual([ { x: 1, y: 2, width: 3, height: 4 }, ]); }); it("ไม่แก้ array และ rect ต้นฉบับ", () => { const input = [ { x: 45, y: 21, width: 20, height: 5 }, { x: 10, y: 20, width: 30, height: 5 }, ]; const snapshot = input.map((r) => ({ ...r })); mergeRectsByLine(input); expect(input).toEqual(snapshot); }); });