forked from bots-garden/ori
| ✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme | 1 | import { fireEvent, render, screen } from "@testing-library/react"; |
| 2 | import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; | |
| 3 | import { | |
| 4 | KEYBOARD_RESIZE_STEP, | |
| 5 | ResizeHandle, | |
| 6 | widthAfterKey, | |
| 7 | } from "./ResizeHandle"; | |
| 8 | ||
| 9 | // jsdom has no PointerEvent; a MouseEvent carrying a pointerId is enough for | |
| 10 | // React's pointer handlers and gives the tests real clientX values. | |
| 11 | beforeAll(() => { | |
| 12 | if (typeof window.PointerEvent === "undefined") { | |
| 13 | class PointerEventPolyfill extends MouseEvent { | |
| 14 | readonly pointerId: number; | |
| 15 | constructor(type: string, init: PointerEventInit = {}) { | |
| 16 | super(type, init); | |
| 17 | this.pointerId = init.pointerId ?? 0; | |
| 18 | } | |
| 19 | } | |
| 20 | Object.defineProperty(window, "PointerEvent", { | |
| 21 | value: PointerEventPolyfill, | |
| 22 | configurable: true, | |
| 23 | }); | |
| 24 | } | |
| 25 | }); | |
| 26 | ||
| 27 | const onResize = vi.fn(); | |
| 28 | const onReset = vi.fn(); | |
| 29 | ||
| 30 | function renderHandle(width = 200) { | |
| 31 | render( | |
| 32 | <ResizeHandle | |
| 33 | label="Resize the file tree" | |
| 34 | width={width} | |
| 35 | min={120} | |
| 36 | max={800} | |
| 37 | onResize={onResize} | |
| 38 | onReset={onReset} | |
| 39 | />, | |
| 40 | ); | |
| 41 | return screen.getByRole("separator", { name: "Resize the file tree" }); | |
| 42 | } | |
| 43 | ||
| 44 | describe("ResizeHandle", () => { | |
| 45 | beforeEach(() => { | |
| 46 | onResize.mockReset(); | |
| 47 | onReset.mockReset(); | |
| 48 | }); | |
| 49 | ||
| 50 | it("exposes the width through the separator pattern", () => { | |
| 51 | const handle = renderHandle(300); | |
| 52 | expect(handle.getAttribute("aria-orientation")).toBe("vertical"); | |
| 53 | expect(handle.getAttribute("aria-valuenow")).toBe("300"); | |
| 54 | expect(handle.getAttribute("aria-valuemin")).toBe("120"); | |
| 55 | expect(handle.getAttribute("aria-valuemax")).toBe("800"); | |
| 56 | }); | |
| 57 | ||
| 58 | it("reports the width while dragging and stops on pointer up", () => { | |
| 59 | const handle = renderHandle(200); | |
| 60 | fireEvent.pointerDown(handle, { clientX: 500, pointerId: 1 }); | |
| 61 | expect(handle.classList.contains("resize-handle-active")).toBe(true); | |
| 62 | ||
| 63 | fireEvent.pointerMove(handle, { clientX: 540, pointerId: 1 }); | |
| 64 | fireEvent.pointerMove(handle, { clientX: 470, pointerId: 1 }); | |
| 65 | expect(onResize.mock.calls).toEqual([[240], [170]]); | |
| 66 | ||
| 67 | fireEvent.pointerUp(handle, { clientX: 470, pointerId: 1 }); | |
| 68 | expect(handle.classList.contains("resize-handle-active")).toBe(false); | |
| 69 | fireEvent.pointerMove(handle, { clientX: 600, pointerId: 1 }); | |
| 70 | expect(onResize).toHaveBeenCalledTimes(2); | |
| 71 | }); | |
| 72 | ||
| 73 | it("ignores pointer moves that did not start on the handle", () => { | |
| 74 | const handle = renderHandle(200); | |
| 75 | fireEvent.pointerMove(handle, { clientX: 600, pointerId: 1 }); | |
| 76 | expect(onResize).not.toHaveBeenCalled(); | |
| 77 | }); | |
| 78 | ||
| 79 | it("resizes step by step with the arrow keys and jumps with Home / End", () => { | |
| 80 | const handle = renderHandle(200); | |
| 81 | fireEvent.keyDown(handle, { key: "ArrowRight" }); | |
| 82 | fireEvent.keyDown(handle, { key: "ArrowLeft" }); | |
| 83 | fireEvent.keyDown(handle, { key: "Home" }); | |
| 84 | fireEvent.keyDown(handle, { key: "End" }); | |
| 85 | expect(onResize.mock.calls).toEqual([ | |
| 86 | [200 + KEYBOARD_RESIZE_STEP], | |
| 87 | [200 - KEYBOARD_RESIZE_STEP], | |
| 88 | [120], | |
| 89 | [800], | |
| 90 | ]); | |
| 91 | }); | |
| 92 | ||
| 93 | it("leaves other keys alone", () => { | |
| 94 | const handle = renderHandle(200); | |
| 95 | fireEvent.keyDown(handle, { key: "Enter" }); | |
| 96 | fireEvent.keyDown(handle, { key: "Tab" }); | |
| 97 | expect(onResize).not.toHaveBeenCalled(); | |
| 98 | }); | |
| 99 | ||
| 100 | it("resets on double-click", () => { | |
| 101 | const handle = renderHandle(200); | |
| 102 | fireEvent.doubleClick(handle); | |
| 103 | expect(onReset).toHaveBeenCalledTimes(1); | |
| 104 | }); | |
| 105 | }); | |
| 106 | ||
| 107 | describe("widthAfterKey", () => { | |
| 108 | it("maps the resize keys and returns null otherwise", () => { | |
| 109 | expect(widthAfterKey("ArrowRight", 200, 120, 800)).toBe(216); | |
| 110 | expect(widthAfterKey("ArrowLeft", 200, 120, 800)).toBe(184); | |
| 111 | expect(widthAfterKey("Home", 200, 120, 800)).toBe(120); | |
| 112 | expect(widthAfterKey("End", 200, 120, 800)).toBe(800); | |
| 113 | expect(widthAfterKey("a", 200, 120, 800)).toBeNull(); | |
| 114 | }); | |
| 115 | }); |