| ... | ... | @@ -0,0 +1,127 @@ |
| 1 | import { act, useLayoutEffect, type PointerEvent } from "react"; |
| 2 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 3 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 4 | |
| 5 | import { useResizableWidth } from "./useResizableWidth"; |
| 6 | |
| 7 | let renderer: ReactTestRenderer; |
| 8 | let result: ReturnType<typeof useResizableWidth>; |
| 9 | let captured = false; |
| 10 | const target = { |
| 11 | setPointerCapture: () => { |
| 12 | captured = true; |
| 13 | }, |
| 14 | hasPointerCapture: () => captured, |
| 15 | releasePointerCapture: () => { |
| 16 | captured = false; |
| 17 | }, |
| 18 | }; |
| 19 | const style = { |
| 20 | cursor: "", |
| 21 | userSelect: "", |
| 22 | removeProperty(property: string) { |
| 23 | if (property === "cursor") this.cursor = ""; |
| 24 | if (property === "user-select") this.userSelect = ""; |
| 25 | }, |
| 26 | }; |
| 27 | const setItem = vi.fn(); |
| 28 | const cancelAnimationFrame = vi.fn(); |
| 29 | let events: EventTarget; |
| 30 | let frame: FrameRequestCallback | undefined; |
| 31 | |
| 32 | function pointer(clientX = 100) { |
| 33 | return { |
| 34 | button: 0, |
| 35 | pointerId: 1, |
| 36 | clientX, |
| 37 | currentTarget: target, |
| 38 | preventDefault() {}, |
| 39 | stopPropagation() {}, |
| 40 | } as unknown as PointerEvent<HTMLElement>; |
| 41 | } |
| 42 | |
| 43 | function Panel() { |
| 44 | const resize = useResizableWidth({ |
| 45 | storageKey: "test-panel-width", |
| 46 | defaultWidth: 400, |
| 47 | minWidth: 200, |
| 48 | maxWidth: 800, |
| 49 | edge: "left", |
| 50 | }); |
| 51 | useLayoutEffect(() => { |
| 52 | result = resize; |
| 53 | }); |
| 54 | return null; |
| 55 | } |
| 56 | |
| 57 | beforeEach(async () => { |
| 58 | captured = false; |
| 59 | frame = undefined; |
| 60 | style.cursor = ""; |
| 61 | style.userSelect = ""; |
| 62 | events = new EventTarget(); |
| 63 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |
| 64 | vi.stubGlobal("window", { |
| 65 | addEventListener: events.addEventListener.bind(events), |
| 66 | removeEventListener: events.removeEventListener.bind(events), |
| 67 | localStorage: { getItem: () => null, setItem }, |
| 68 | }); |
| 69 | vi.stubGlobal("document", { body: { style } }); |
| 70 | vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { |
| 71 | frame = callback; |
| 72 | return 42; |
| 73 | }); |
| 74 | vi.stubGlobal("cancelAnimationFrame", cancelAnimationFrame); |
| 75 | await act(() => { |
| 76 | renderer = create(<Panel />); |
| 77 | }); |
| 78 | }); |
| 79 | |
| 80 | afterEach(async () => { |
| 81 | await act(() => renderer.unmount()); |
| 82 | vi.unstubAllGlobals(); |
| 83 | vi.clearAllMocks(); |
| 84 | }); |
| 85 | |
| 86 | describe("panel resize cleanup", () => { |
| 87 | it.each(["unmount", "lost capture", "blur", "cancel"])( |
| 88 | "clears the cursor and pending resize after %s", |
| 89 | async (reason) => { |
| 90 | await act(() => { |
| 91 | result.handlers.onPointerDown(pointer()); |
| 92 | result.handlers.onPointerMove(pointer(50)); |
| 93 | }); |
| 94 | await act(() => frame?.(0)); |
| 95 | expect(result.width).toBe(450); |
| 96 | // Queue another move to check that interruption cancels pending work too. |
| 97 | await act(() => result.handlers.onPointerMove(pointer(25))); |
| 98 | expect(style.cursor).toBe("col-resize"); |
| 99 | expect(style.userSelect).toBe("none"); |
| 100 | await act(() => { |
| 101 | if (reason === "unmount") renderer.unmount(); |
| 102 | if (reason === "lost capture") result.handlers.onLostPointerCapture(pointer()); |
| 103 | if (reason === "blur") events.dispatchEvent(new Event("blur")); |
| 104 | if (reason === "cancel") result.handlers.onPointerCancel(pointer()); |
| 105 | }); |
| 106 | expect(style.cursor).toBe(""); |
| 107 | expect(style.userSelect).toBe(""); |
| 108 | expect(captured).toBe(false); |
| 109 | expect(cancelAnimationFrame).toHaveBeenCalledWith(42); |
| 110 | expect(setItem).not.toHaveBeenCalled(); |
| 111 | if (reason !== "unmount") expect(result.width).toBe(400); |
| 112 | }, |
| 113 | ); |
| 114 | |
| 115 | it("saves the final width when release is followed by lost capture", async () => { |
| 116 | await act(() => { |
| 117 | result.handlers.onPointerDown(pointer()); |
| 118 | result.handlers.onPointerMove(pointer(50)); |
| 119 | result.handlers.onPointerUp(pointer(50)); |
| 120 | result.handlers.onLostPointerCapture(pointer(50)); |
| 121 | }); |
| 122 | expect(result.width).toBe(450); |
| 123 | expect(setItem).toHaveBeenCalledExactlyOnceWith("test-panel-width", "450"); |
| 124 | expect(style.cursor).toBe(""); |
| 125 | expect(captured).toBe(false); |
| 126 | }); |
| 127 | }); |