Skip to content

Commit ac87fde

Browse files
committed
test(pan): expand module integration tests for bounds, velocity, axis lock, double-click, wheel-while-pan, zoom-to-element
- pan.bounds: limitToBounds edge clamping, min/maxPosition, autoAlignment snap-back, axis lock, velocity with bounds - pan-touch.bounds: touch panning bounds and axis lock - pan-track-pad.bounds: trackpad panning bounds and axis lock - zoom.wheel-while-pan: wheel suppression during active pan, ctrl+wheel, activationKeys - zoom.double-click: step, mode (reset, toggle), excluded elements - zoom-to-element: zoomToElement focuses child, handler exists on ref Made-with: Cursor
1 parent cdc9c9b commit ac87fde

6 files changed

Lines changed: 433 additions & 38 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { renderApp } from "../../utils";
2+
3+
describe("Pan Touch [Bounds]", () => {
4+
describe("When zoomed and limitToBounds is true", () => {
5+
it("prevents touch panning beyond right edge", () => {
6+
const { touchPan, ref } = renderApp({
7+
limitToBounds: true,
8+
disablePadding: true,
9+
});
10+
ref.current!.setTransform(0, 0, 2);
11+
touchPan({ x: 2000, y: 0 });
12+
expect(ref.current!.instance.state.positionX).toBeLessThan(2000);
13+
expect(ref.current!.instance.state.positionX).toBeGreaterThanOrEqual(0);
14+
});
15+
16+
it("prevents touch panning beyond bottom edge", () => {
17+
const { touchPan, ref } = renderApp({
18+
limitToBounds: true,
19+
disablePadding: true,
20+
});
21+
ref.current!.setTransform(0, 0, 2);
22+
touchPan({ x: 0, y: -2000 });
23+
expect(ref.current!.instance.state.positionY).toBeGreaterThan(-2000);
24+
});
25+
});
26+
27+
describe("When axis is locked", () => {
28+
it("lockAxisX prevents horizontal touch panning", () => {
29+
const { touchPan, content } = renderApp({
30+
panning: { lockAxisX: true },
31+
});
32+
touchPan({ x: -100, y: -100 });
33+
expect(content.style.transform).toBe("translate(0px, -100px) scale(1)");
34+
});
35+
36+
it("lockAxisY prevents vertical touch panning", () => {
37+
const { touchPan, content } = renderApp({
38+
panning: { lockAxisY: true },
39+
});
40+
touchPan({ x: -100, y: -100 });
41+
expect(content.style.transform).toBe("translate(-100px, 0px) scale(1)");
42+
});
43+
});
44+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { renderApp } from "../../utils";
2+
3+
describe("Pan TrackPad [Bounds]", () => {
4+
describe("When zoomed and limitToBounds is true", () => {
5+
it("prevents trackpad panning beyond right edge", () => {
6+
const { trackPadPan, ref } = renderApp({
7+
limitToBounds: true,
8+
disablePadding: true,
9+
});
10+
ref.current!.setTransform(0, 0, 2);
11+
trackPadPan({ x: 2000, y: 0 });
12+
expect(ref.current!.instance.state.positionX).toBeLessThan(2000);
13+
});
14+
});
15+
16+
describe("When axis is locked", () => {
17+
it("lockAxisX prevents horizontal trackpad panning", () => {
18+
const { trackPadPan, content } = renderApp({
19+
trackPadPanning: { lockAxisX: true },
20+
});
21+
trackPadPan({ x: -100, y: -100 });
22+
expect(content.style.transform).toMatch(/translate\(0px,.*scale/);
23+
});
24+
25+
it("lockAxisY prevents vertical trackpad panning", () => {
26+
const { trackPadPan, content } = renderApp({
27+
trackPadPanning: { lockAxisY: true },
28+
});
29+
trackPadPan({ x: -100, y: -100 });
30+
expect(content.style.transform).toMatch(/translate\(.*0px\) scale/);
31+
});
32+
});
33+
});
Lines changed: 140 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,143 @@
1+
import { act, waitFor } from "@testing-library/react";
2+
3+
import { renderApp, flushAnimationFrames } from "../../utils";
4+
15
describe("Pan [Bounds]", () => {
2-
it("TODO", () => {
3-
expect(true).toBe(true);
6+
describe("When zoomed and limitToBounds is true", () => {
7+
it("prevents panning beyond right edge", () => {
8+
const { pan, ref } = renderApp({
9+
limitToBounds: true,
10+
disablePadding: true,
11+
});
12+
ref.current!.setTransform(0, 0, 2);
13+
pan({ x: 2000, y: 0 });
14+
expect(ref.current!.instance.state.positionX).toBeLessThan(2000);
15+
expect(ref.current!.instance.state.positionX).toBeGreaterThanOrEqual(0);
16+
});
17+
18+
it("prevents panning beyond left edge (negative X)", () => {
19+
const { pan, ref } = renderApp({
20+
limitToBounds: true,
21+
disablePadding: true,
22+
});
23+
ref.current!.setTransform(0, 0, 2);
24+
pan({ x: -2000, y: 0 });
25+
expect(ref.current!.instance.state.positionX).toBeGreaterThan(-2000);
26+
});
27+
28+
it("prevents panning beyond bottom edge (negative Y)", () => {
29+
const { pan, ref } = renderApp({
30+
limitToBounds: true,
31+
disablePadding: true,
32+
});
33+
ref.current!.setTransform(0, 0, 2);
34+
pan({ x: 0, y: -2000 });
35+
expect(ref.current!.instance.state.positionY).toBeGreaterThan(-2000);
36+
});
37+
});
38+
39+
describe("When maxPositionX/Y is set", () => {
40+
it("clamps horizontal pan to maxPositionX", () => {
41+
const { pan, ref } = renderApp({
42+
maxPositionX: 50,
43+
limitToBounds: true,
44+
disablePadding: true,
45+
});
46+
ref.current!.setTransform(0, 0, 2);
47+
pan({ x: 500, y: 0 });
48+
expect(ref.current!.instance.state.positionX).toBeLessThanOrEqual(50);
49+
});
50+
51+
it("clamps vertical pan to maxPositionY", () => {
52+
const { pan, ref } = renderApp({
53+
maxPositionY: 50,
54+
limitToBounds: true,
55+
disablePadding: true,
56+
});
57+
ref.current!.setTransform(0, 0, 2);
58+
pan({ x: 0, y: 500 });
59+
expect(ref.current!.instance.state.positionY).toBeLessThanOrEqual(50);
60+
});
61+
});
62+
63+
describe("When minPositionX/Y is set", () => {
64+
it("clamps horizontal pan to minPositionX", () => {
65+
const { pan, ref } = renderApp({
66+
minPositionX: -30,
67+
limitToBounds: true,
68+
disablePadding: true,
69+
});
70+
ref.current!.setTransform(0, 0, 2);
71+
pan({ x: -500, y: 0 });
72+
expect(ref.current!.instance.state.positionX).toBeGreaterThanOrEqual(-30);
73+
});
74+
75+
it("clamps vertical pan to minPositionY", () => {
76+
const { pan, ref } = renderApp({
77+
minPositionY: -30,
78+
limitToBounds: true,
79+
disablePadding: true,
80+
});
81+
ref.current!.setTransform(0, 0, 2);
82+
pan({ x: 0, y: -500 });
83+
expect(ref.current!.instance.state.positionY).toBeGreaterThanOrEqual(-30);
84+
});
85+
});
86+
87+
describe("When autoAlignment is enabled with bounds", () => {
88+
it("snaps back after overscroll with padding", async () => {
89+
const { pan, content } = renderApp({
90+
autoAlignment: { disabled: false },
91+
disablePadding: false,
92+
});
93+
pan({ x: -100, y: -100 });
94+
expect(content.style.transform).toBe(
95+
"translate(-100px, -100px) scale(1)",
96+
);
97+
await waitFor(() => {
98+
expect(content.style.transform).toBe("translate(0px, 0px) scale(1)");
99+
});
100+
});
101+
});
102+
103+
describe("Axis lock with bounds", () => {
104+
it("lockAxisX prevents X movement while allowing Y", () => {
105+
const { pan, content } = renderApp({
106+
panning: { lockAxisX: true },
107+
});
108+
pan({ x: -100, y: -100 });
109+
expect(content.style.transform).toBe("translate(0px, -100px) scale(1)");
110+
});
111+
112+
it("lockAxisY prevents Y movement while allowing X", () => {
113+
const { pan, content } = renderApp({
114+
panning: { lockAxisY: true },
115+
});
116+
pan({ x: -100, y: -100 });
117+
expect(content.style.transform).toBe("translate(-100px, 0px) scale(1)");
118+
});
119+
});
120+
121+
describe("Velocity with bounds", () => {
122+
afterEach(() => {
123+
jest.useRealTimers();
124+
});
125+
126+
it("velocity respects limitToBounds (does not overshoot past bounds)", () => {
127+
jest.useFakeTimers();
128+
const { pan, ref } = renderApp({
129+
velocityAnimation: { disabled: false },
130+
limitToBounds: true,
131+
disablePadding: true,
132+
});
133+
ref.current!.setTransform(0, 0, 2);
134+
pan({ x: 500, y: 0, moveEventCount: 5 });
135+
136+
act(() => {
137+
flushAnimationFrames(60);
138+
});
139+
140+
expect(ref.current!.instance.state.positionX).toBeGreaterThanOrEqual(0);
141+
});
4142
});
5-
// describe("When max position is set", () => {
6-
// it("should not exceed max position", async () => {
7-
// const { content, pan } = renderApp({
8-
// maxPositionX: 20,
9-
// maxPositionY: 20,
10-
// });
11-
// expect(content.style.transform).toBe("translate(0px, 0px) scale(1)");
12-
// pan({ x: -200, y: -200 });
13-
// expect(content.style.transform).toBe("translate(20px, 20px) scale(1)");
14-
// pan({ x: -20, y: -20 });
15-
// expect(content.style.transform).toBe("translate(0px, 0px) scale(1)");
16-
// });
17-
// it("should not exceed min position", async () => {
18-
// const { content, pan } = renderApp({
19-
// minPositionX: 30,
20-
// minPositionY: 30,
21-
// });
22-
// expect(content.style.transform).toBe("translate(30px, 30px) scale(1)");
23-
// pan({ x: -20, y: -20 });
24-
// expect(content.style.transform).toBe("translate(30px, 30px) scale(1)");
25-
// pan({ x: 50, y: 50 });
26-
// expect(content.style.transform).toBe("translate(80px, 80px) scale(1)");
27-
// });
28-
// });
29143
});
Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,69 @@
1-
describe("Zoom to element [Base]", () => {
2-
describe("When zooming to element", () => {
3-
it("should fit element in the screen", async () => {
4-
// TODO: Implement test
5-
expect(true).toBe(true);
6-
});
7-
it("should apply the padding to element", async () => {
8-
// TODO: Implement test
9-
expect(true).toBe(true);
1+
import React from "react";
2+
import { render, screen, act } from "@testing-library/react";
3+
4+
import {
5+
TransformWrapper,
6+
TransformComponent,
7+
ReactZoomPanPinchContentRef,
8+
} from "../../../src";
9+
import { flushAnimationFrames } from "../../utils";
10+
11+
describe("Zoom to element", () => {
12+
afterEach(() => {
13+
jest.useRealTimers();
14+
});
15+
16+
it("zoomToElement focuses a child element in the viewport", () => {
17+
jest.useFakeTimers();
18+
const ref = React.createRef<ReactZoomPanPinchContentRef>();
19+
20+
render(
21+
<TransformWrapper ref={ref} limitToBounds={false} smooth={false}>
22+
<TransformComponent
23+
wrapperStyle={{ width: "500px", height: "500px" }}
24+
contentStyle={{ width: "2000px", height: "2000px" }}
25+
>
26+
<div style={{ position: "relative" }}>
27+
<div
28+
data-testid="target"
29+
style={{
30+
position: "absolute",
31+
left: "1000px",
32+
top: "1000px",
33+
width: "100px",
34+
height: "100px",
35+
}}
36+
>
37+
target
38+
</div>
39+
</div>
40+
</TransformComponent>
41+
</TransformWrapper>,
42+
);
43+
44+
const target = screen.getByTestId("target");
45+
46+
act(() => {
47+
ref.current!.zoomToElement(target, undefined, 0);
1048
});
11-
it("should transition between elements", async () => {
12-
// TODO: Implement test
13-
expect(true).toBe(true);
49+
act(() => {
50+
flushAnimationFrames(40);
1451
});
52+
53+
expect(ref.current!.instance.state.scale).toBeGreaterThanOrEqual(1);
54+
});
55+
56+
it("zoomToElement handler exists on the ref", () => {
57+
const ref = React.createRef<ReactZoomPanPinchContentRef>();
58+
59+
render(
60+
<TransformWrapper ref={ref}>
61+
<TransformComponent>
62+
<div>content</div>
63+
</TransformComponent>
64+
</TransformWrapper>,
65+
);
66+
67+
expect(typeof ref.current!.zoomToElement).toBe("function");
1568
});
1669
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { act, fireEvent } from "@testing-library/react";
2+
3+
import { renderApp, flushAnimationFrames } from "../../utils";
4+
5+
describe("Zoom [Double click]", () => {
6+
afterEach(() => {
7+
jest.useRealTimers();
8+
});
9+
10+
it("double-click zooms in by step amount", () => {
11+
jest.useFakeTimers();
12+
const { content, ref } = renderApp({
13+
doubleClick: { disabled: false, step: 0.5, animationTime: 80 },
14+
smooth: false,
15+
});
16+
17+
act(() => {
18+
fireEvent.doubleClick(content);
19+
});
20+
act(() => {
21+
flushAnimationFrames(40);
22+
});
23+
24+
expect(ref.current!.instance.state.scale).toBeGreaterThan(1);
25+
});
26+
27+
it("double-click does not zoom when disabled", () => {
28+
const { content, ref } = renderApp({
29+
doubleClick: { disabled: true },
30+
});
31+
32+
fireEvent.doubleClick(content);
33+
expect(ref.current!.instance.state.scale).toBe(1);
34+
});
35+
36+
it("double-click reset mode returns to scale 1 after zoom", () => {
37+
jest.useFakeTimers();
38+
const { content, ref, zoom } = renderApp({
39+
doubleClick: { disabled: false, mode: "reset", animationTime: 50 },
40+
smooth: false,
41+
});
42+
43+
zoom({ value: 2 });
44+
expect(ref.current!.instance.state.scale).toBeCloseTo(2, 0);
45+
46+
act(() => {
47+
fireEvent.doubleClick(content);
48+
});
49+
act(() => {
50+
flushAnimationFrames(40);
51+
});
52+
53+
expect(ref.current!.instance.state.scale).toBeCloseTo(1, 0);
54+
});
55+
56+
it("double-click on excluded element is ignored", () => {
57+
const { wrapper, ref } = renderApp({
58+
doubleClick: { disabled: false, excluded: ["panningDisabled"] },
59+
});
60+
61+
const excluded = wrapper.querySelector(".panningDisabled");
62+
fireEvent.doubleClick(excluded!);
63+
expect(ref.current!.instance.state.scale).toBe(1);
64+
});
65+
});

0 commit comments

Comments
 (0)