eCourt-note/tests/hooks/useCustomSelection-dom.test.ts
Your Name 45e60330b8 highlight-touch: แก้กลุ่มบั๊กจอสัมผัสของ useCustomSelection + rectsInSweptPath ตามรายงาน review 12 ข้อ + follow-up 3 ข้อ — ลากแนวนอนบรรทัดเดียวบน tablet ต้องสร้างมาร์กได้ทุกครั้ง และ gesture ต้องไม่ตายถาวรกลางsession — (1) fingerOffsetFor: FINGER_OFFSET_PX=12 คงที่ทำ probe ที่ y−12 หลุดบรรทัด (บรรทัด PDF บน tablet zoom-fit สูงจริง ~14px — วัดจาก viewport 820px) ไปโดน heading จนลากแนวนอนเงียบไม่สร้าง — offset กลายเป็น min(12, ครึ่งความสูงบรรทัด−2) วัดจาก span ของ anchor ตอน pointerdown (บรรทัด ≥28px ยังได้ 12 เท่าเดิม) และถ้าน้อยกว่า 12 จะ resolve anchor ซ้ำที่จุดที่หักจริง — ยืนยันด้วย Playwright emulate iPad + CDP touch จริง (ก่อนแก้ 9/11 หลังแก้ 11/11) (2) A1 rangeBetween ฝั่งหลังใช้ boundary end แทน start — anchor/focus ผ่าน word snap แล้วเป็น range ครอบคำ การอ่าน start ทิ้งคำท้าย (ลาก alpha→gamma ได้แค่ alpha beta) และลากในคำเดียวได้ selection ว่าง (3) A2 anchorPathPt(client คงที่)→anchorRel สัดส่วน 0–1 ของหน้า แปลงกลับทุกเฟรม — พิกัด client โกหกทันทีที่ auto-scroll เลื่อนเนื้อหากลางลาก จนบรรทัดต้นทางหายจากทั้ง preview และค่าที่บันทึก (จำลอง 26 บรรทัดเคยได้ 14) (4) A3 rectsInSweptPath (utils/coordinates.ts) rewrite: snap จุด start/end เข้า center บรรทัดใกล้สุด (snapY) ก่อนตัดสินบรรทัดเดียวกัน/คนละบรรทัด — พิกัดนิ้วที่คนละบรรทัดแต่ dy น้อยกว่า lineTol เคยถูกตีเป็นบรรทัดเดียวจนกรองได้ 0 rect (ลากท้ายบรรทัดบน→ต้นบรรทัดถัดไประยะสั้น) — ใช้ร่วม desktop เหมือนเดิม เคส test เดิม 3 เคสยังผ่าน (5) A4 range.collapsed (ย่อจนว่างจริง) = เคลียร์ lastRef + preview แล้ว return — commit ตรง preview เสมอ แยกจากเคสกรองว่างชั่วคราวที่ยังคง lastRef ตามเจตนาเดิม (6) A5 caretInSpanAt วาง caret ตามสัดส่วน x ใน span แทนต้น span เสมอ (pdf.js มักครอบทั้งบรรทัดใน span เดียว) ใช้ทั้งชั้น ③ elementsFromPoint และ ④ nearest span (7) A6 fromElement ปฏิเสธ text node ที่อยู่นอก .textLayer (กัน DFS หลุดไป text node แรกของหน้า) (8) A7.1 auto-scroll speed cap ระยะเลยขอบที่ margin / A7.2 ลบ seenMovePointerIds debug / A7.4 mouse ปุ่มซ้ายเท่านั้น (hybrid) / A7.5 ส่ง DragState ตรงให้ resolveTextCaretDeep ให้ spanCache ถูกเก็บกลับ (9) B1 ระหว่างลาก retry resolver แค่ 2 ทิศแรก (ครบ 8 ทิศ ~6ms/เฟรม เก็บไว้ pointerdown — fail ระหว่างลากมีทางออกคือคง lastFocus) (10) B2+2.3 spanCache เก็บ fraction 0–1 ของหน้า (แบบเดียวกับ anchorRel — ทน re-fit ความกว้างกลาง gesture เช่น นิ้วที่สอง toggle sidebar) วัดครั้งเดียวต่อ gesture แทน getBoundingClientRect ทั้งหน้าทุกครั้ง (~2.3ms) (11) code review 2.1: pointerup/pointercancel ย้ายจาก <main> เป็นตาข่าย document แบบ capture phase ผูกตั้งแต่ pointerdown จน finish — Chromium กับ touch ถอด pointer capture กลาง drag ได้เอง พอ capture หาด pointerup ยิงนอก <main> จน dragRef ค้าง gesture ถัดไปเงียบหมด (arm เฉพาะตอน active ไม่พอ — tap ที่ไม่ขยับก็ต้องมีตาข่ายรับ ไม่งั้นค้างเหมือนกัน เจอจริงตอนแก้ e2e 9/13) (12) A7.3 ไม่ใช้: ห้ามผูก finish เข้า lostpointercapture — กับ touch ของ Chromium ยิงกลาง drag ได้เอง (implicit capture ของ span + re-hit-test) ผูกแล้ว dragRef ถูกเคลียร์กลางทาง gesture ตายหมด — ยืนยันด้วย CDP touch event log จริง (13) 2.2 แก้ comment fingerOffsetFor ให้ตรงกลไก (เคส probe โดน heading ≥28px ยังใช้ heuristic เดิม) (14) tests: coordinates +7 เคส (รวม 2 บรรทัด dy เล็กที่เคยได้ 0 และ auto-scroll 26 บรรทัด) + ไฟล์ใหม่ tests/hooks/useCustomSelection-dom.test.ts 7 เคส snapCaretToWord/rangeBetween (export เพิ่มเพื่อ test พร้อม comment) ผ่าน pragma @vitest-environment jsdom รวมคำไทยกับ Intl.Segmenter "แมวกินปลา"→"แมว" — เพิ่ม devDependency jsdom (test-only) (15) rules/pdf-viewer.md sync จุดที่พฤติกรรมเปลี่ยนครบ — ผ่าน code review รอบ 2 (ไม่มี bug ระดับต้องแก้) + tsc + vitest 157/157 + e2e tablet 12/13 (เคส F fail ที่การจัดเตรียมเงื่อนไขของ script เอง ไม่ใช่ฟังก์ชัน — A3 ครอบโดย unit tests) (คำขอผู้ใช้ 2026-09-28)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:01:09 +07:00

82 lines
3.3 KiB
TypeScript
Raw Permalink 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.

// @vitest-environment jsdom
// เทสต์ pure helpers ของ useCustomSelection ที่ต้องมี DOM Range จริง — แยกจาก
// ชุด pure utils (environment node ตาม vitest.config.ts) ด้วย pragma ต่อไฟล์
// ตัว hook เอง (Pointer Events + rAF + gesture จริง) ยืนยันด้วย Playwright แทน
import { describe, expect, it } from "vitest";
import { rangeBetween, snapCaretToWord } from "@/hooks/useCustomSelection";
/** text node เดียวใน body + collapsed caret ที่ offset (เลียน anchor/focus ของ
* useCustomSelection ก่อนผ่าน word snap) */
function setup(text: string, offset: number): { caret: () => Range } {
const node = document.createTextNode(text);
document.body.replaceChildren(node);
return {
caret: () => {
const r = document.createRange();
r.setStart(node, offset);
r.collapse(true);
return r;
},
};
}
describe("snapCaretToWord", () => {
it("จิ้มกลางคำ → ขยายครอบทั้งคำ (อังกฤษ)", () => {
const { caret } = setup("alpha beta gamma", 2);
const c = caret();
snapCaretToWord(c);
expect(c.toString()).toBe("alpha");
});
it("จิ้มกลางคำไทย (ไม่มีช่องว่าง) → Intl.Segmenter แยกคำให้ (node ≥16 มี full-icu)", () => {
const { caret } = setup("แมวกินปลา", 1);
const c = caret();
snapCaretToWord(c);
expect(c.toString()).toBe("แมว");
});
it("จิ้มกลางช่องว่างล้วน → ไม่ขยาย (ยัง collapsed)", () => {
const { caret } = setup("alpha beta", 6);
const c = caret();
snapCaretToWord(c);
expect(c.collapsed).toBe(true);
});
});
describe("rangeBetween — ฝั่งหลังต้องใช้ end boundary (A1: อย่าทิ้งคำท้าย)", () => {
const TEXT = "alpha beta gamma delta";
// offset: alpha 0–5, beta 6–10, gamma 11–16, delta 17–22
/** เลียน flow จริง: สร้าง caret สองจุด → word snap ทั้งคู่ → rangeBetween */
function between(offA: number, offB: number): string {
const node = document.createTextNode(TEXT);
document.body.replaceChildren(node);
const mk = (off: number) => {
const r = document.createRange();
r.setStart(node, off);
r.collapse(true);
return r;
};
const a = mk(offA);
const b = mk(offB);
snapCaretToWord(a);
snapCaretToWord(b);
return rangeBetween(a, b)?.toString() ?? "<null>";
}
it("ลาก alpha→gamma ได้ครบถึงคำท้าย (เดิมหาย gamma)", () => {
expect(between(2, 13)).toBe("alpha beta gamma");
});
it("ลากย้อน gamma→alpha ได้ผลเดียวกัน", () => {
expect(between(13, 2)).toBe("alpha beta gamma");
});
it("ลากในคำเดียว (beta→beta) ได้ทั้งคำ (เดิม selection ว่าง)", () => {
expect(between(7, 9)).toBe("beta");
});
it("ลาก alpha→beta ได้สองคำ", () => {
expect(between(2, 8)).toBe("alpha beta");
});
});