import { describe, expect, test } from "bun:test"; import { EditingTextMode, editTextPlugin } from "../plugins/editTextPlugin"; import { filterElements } from "../utils/elementUtils"; import { offsetRangeToSelection, selectionToOffsetRange, } from "../utils/textSelection"; import { createBeforeInputEvent, createKeyboardEvent, createPointerEvent, createTestCanvas, } from "./canvasTestUtils"; function createTextHarness(trigger: "click" | "doubleClick" = "doubleClick") { const element = { id: "t1", rect: { x: 0, y: 0, width: 100, height: 20 }, text: "hello world", }; const updatedTexts: string[] = []; const onBlurIds: string[] = []; const measuredCharacters = Array.from(element.text).map( (character, index) => ({ path: [0], column: index, offset: index, line: 0, character, rect: { x: index, y: 0, width: 1, height: 1, left: index, right: index + 1, top: 0, bottom: 1, toJSON: () => ({}), } as DOMRect, physicalLine: 0, }) ); const hitTest = ( _element: typeof element, { localPoint }: { localPoint: { x: number; y: number } } ) => { return Math.min(Math.floor(localPoint.x / 10), element.text.length); }; const harness = createTestCanvas( (canvas) => canvas.addPlugins( editTextPlugin({ triggerEvent: trigger }) ), { data: { selectedElementIds: [element.id], zoom: 1 }, contextProperties: { getElementId: (item: typeof element) => item.id, isTextEditable: () => true, filterElements: (options) => filterElements([element], options), getTextElement: (id) => (id === element.id ? element : undefined), updateText: (_id, nextText) => updatedTexts.push(nextText), measureText: () => ({ measuredCharacters }), hitTestText: hitTest, onBlur: (id) => onBlurIds.push(id), }, } ); return { harness, element, updatedTexts, onBlurIds }; } function doubleClick(harness: ReturnType) { const originalNow = Date.now; let now = 0; Date.now = () => now; harness.handlers.onPointerDown?.(createPointerEvent({ x: 10, y: 10 })); now = 100; harness.handlers.onPointerDown?.(createPointerEvent({ x: 10, y: 10 })); Date.now = originalNow; } describe("editText plugin", () => { test("enters editing on trigger click and sets selection", () => { const { harness } = createTextHarness("click"); harness.handlers.onPointerDown?.(createPointerEvent({ x: 10, y: 10 })); expect(harness.getMode().type).toBe("editingText"); expect(harness.getShouldHandleKeyboardEvents()).toBe(false); expect(harness.getData().textDragAnchor).toBeGreaterThanOrEqual(0); }); test("double click enters editing and drag updates selection", () => { const { harness } = createTextHarness(); doubleClick(harness); expect(harness.getMode().type).toBe("editingText"); const initialRange = selectionToOffsetRange( (harness.getMode() as EditingTextMode).range ?? null, "hello world".length ); expect(initialRange?.anchor).toBeDefined(); harness.handlers.onPointerMove?.(createPointerEvent({ x: 40, y: 10 })); const draggedRange = selectionToOffsetRange( (harness.getMode() as EditingTextMode).range ?? null, "hello world".length ); expect(draggedRange).toBeDefined(); }); test("onBeforeInput inserts text and updates selection", () => { const { harness, updatedTexts, element } = createTextHarness(); harness.setMode({ type: "editingText", editingElementId: element.id, range: offsetRangeToSelection({ anchor: 0, focus: 0 }), } as any); harness.handlers.onBeforeInput?.( createBeforeInputEvent({ inputType: "insertText", data: "X" }) ); expect(updatedTexts.pop()).toBe("Xhello world"); const range = selectionToOffsetRange( (harness.getMode() as EditingTextMode).range ?? null, "Xhello world".length ); expect(range).toMatchObject({ anchor: 1, focus: 1 }); }); test("onKeyDown moves selection", () => { const { harness, element } = createTextHarness(); harness.setMode({ type: "editingText", editingElementId: element.id, range: offsetRangeToSelection({ anchor: 0, focus: 0 }), } as any); harness.handlers.onKeyDown?.(createKeyboardEvent("ArrowRight")); const range = selectionToOffsetRange( (harness.getMode() as EditingTextMode).range ?? null, element.text.length ); expect(range).toMatchObject({ anchor: 1, focus: 1 }); }); test("click outside blurs and restores keyboard handling", () => { const { harness, onBlurIds, element } = createTextHarness(); harness.setMode({ type: "editingText", editingElementId: element.id, range: null, } as any); harness.handlers.onPointerDown?.(createPointerEvent({ x: 200, y: 200 })); expect(harness.getMode().type).toBe("none"); expect(onBlurIds).toEqual([element.id]); expect(harness.getShouldHandleKeyboardEvents()).toBe(true); }); });