Skip to content

Commit cdc9c9b

Browse files
committed
test(api): add public props coverage tests for full API surface
117 tests across 18 spec files covering every leaf prop on ReactZoomPanPinchProps (TransformWrapper), TransformComponent, MiniMap, and KeepScale — children, ref, disabled, initial transform, bounds/positions, scale limits, rendering, wheel, panning, pinch, trackpad panning, double-click, zoom animation, auto alignment, velocity animation, and all callbacks. Made-with: Cursor
1 parent 665bff5 commit cdc9c9b

18 files changed

Lines changed: 1715 additions & 0 deletions
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { waitFor } from "@testing-library/react";
2+
3+
import { renderApp } from "../../utils";
4+
5+
describe("ReactZoomPanPinchProps.autoAlignment", () => {
6+
describe("autoAlignment.disabled", () => {
7+
it("does not snap back after overscroll when disabled", () => {
8+
const { pan, content } = renderApp({
9+
autoAlignment: { disabled: true },
10+
disablePadding: false,
11+
});
12+
pan({ x: -100, y: -100 });
13+
expect(content.style.transform).toBe(
14+
"translate(-100px, -100px) scale(1)",
15+
);
16+
});
17+
18+
it("snaps back after overscroll when enabled", async () => {
19+
const { pan, content } = renderApp({
20+
autoAlignment: { disabled: false },
21+
disablePadding: false,
22+
});
23+
pan({ x: -100, y: -100 });
24+
expect(content.style.transform).toBe(
25+
"translate(-100px, -100px) scale(1)",
26+
);
27+
await waitFor(() => {
28+
expect(content.style.transform).toBe(
29+
"translate(0px, 0px) scale(1)",
30+
);
31+
});
32+
});
33+
});
34+
35+
describe("autoAlignment.sizeX", () => {
36+
it("accepts sizeX without crashing", () => {
37+
const { ref } = renderApp({
38+
autoAlignment: { disabled: false, sizeX: 50 },
39+
});
40+
expect(ref.current).not.toBeNull();
41+
});
42+
});
43+
44+
describe("autoAlignment.sizeY", () => {
45+
it("accepts sizeY without crashing", () => {
46+
const { ref } = renderApp({
47+
autoAlignment: { disabled: false, sizeY: 50 },
48+
});
49+
expect(ref.current).not.toBeNull();
50+
});
51+
});
52+
53+
describe("autoAlignment.animationTime", () => {
54+
it("accepts animationTime without crashing", () => {
55+
const { ref } = renderApp({
56+
autoAlignment: { disabled: false, animationTime: 200 },
57+
});
58+
expect(ref.current).not.toBeNull();
59+
});
60+
});
61+
62+
describe("autoAlignment.velocityAlignmentTime", () => {
63+
it("accepts velocityAlignmentTime without crashing", () => {
64+
const { ref } = renderApp({
65+
autoAlignment: { disabled: false, velocityAlignmentTime: 100 },
66+
});
67+
expect(ref.current).not.toBeNull();
68+
});
69+
});
70+
71+
describe("autoAlignment.animationType", () => {
72+
it("accepts easeOut animation type", () => {
73+
const { ref } = renderApp({
74+
autoAlignment: { disabled: false, animationType: "easeOut" },
75+
});
76+
expect(ref.current).not.toBeNull();
77+
});
78+
});
79+
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { waitFor } from "@testing-library/react";
2+
3+
import { renderApp } from "../../utils";
4+
5+
const NativeResizeObserver = global.ResizeObserver;
6+
7+
beforeAll(() => {
8+
global.ResizeObserver = class {
9+
observe() {}
10+
disconnect() {}
11+
unobserve() {}
12+
} as unknown as typeof ResizeObserver;
13+
});
14+
15+
afterAll(() => {
16+
global.ResizeObserver = NativeResizeObserver;
17+
});
18+
19+
describe("ReactZoomPanPinchProps.minPositionX", () => {
20+
it("clamps pan so positionX does not go below minPositionX", () => {
21+
const { pan, ref } = renderApp({
22+
minPositionX: -50,
23+
limitToBounds: true,
24+
disablePadding: true,
25+
});
26+
ref.current!.setTransform(0, 0, 2);
27+
pan({ x: -500, y: 0 });
28+
expect(ref.current!.instance.state.positionX).toBeGreaterThanOrEqual(-50);
29+
});
30+
});
31+
32+
describe("ReactZoomPanPinchProps.maxPositionX", () => {
33+
it("clamps pan so positionX does not exceed maxPositionX", () => {
34+
const { pan, ref } = renderApp({
35+
maxPositionX: 50,
36+
limitToBounds: true,
37+
disablePadding: true,
38+
});
39+
ref.current!.setTransform(0, 0, 2);
40+
pan({ x: 500, y: 0 });
41+
expect(ref.current!.instance.state.positionX).toBeLessThanOrEqual(50);
42+
});
43+
});
44+
45+
describe("ReactZoomPanPinchProps.minPositionY", () => {
46+
it("clamps pan so positionY does not go below minPositionY", () => {
47+
const { pan, ref } = renderApp({
48+
minPositionY: -50,
49+
limitToBounds: true,
50+
disablePadding: true,
51+
});
52+
ref.current!.setTransform(0, 0, 2);
53+
pan({ x: 0, y: -500 });
54+
expect(ref.current!.instance.state.positionY).toBeGreaterThanOrEqual(-50);
55+
});
56+
});
57+
58+
describe("ReactZoomPanPinchProps.maxPositionY", () => {
59+
it("clamps pan so positionY does not exceed maxPositionY", () => {
60+
const { pan, ref } = renderApp({
61+
maxPositionY: 50,
62+
limitToBounds: true,
63+
disablePadding: true,
64+
});
65+
ref.current!.setTransform(0, 0, 2);
66+
pan({ x: 0, y: 500 });
67+
expect(ref.current!.instance.state.positionY).toBeLessThanOrEqual(50);
68+
});
69+
});
70+
71+
describe("ReactZoomPanPinchProps.limitToBounds", () => {
72+
it("prevents content from being panned outside wrapper when true", () => {
73+
const { pan, ref } = renderApp({
74+
limitToBounds: true,
75+
disablePadding: true,
76+
});
77+
ref.current!.setTransform(0, 0, 2);
78+
pan({ x: 2000, y: 2000 });
79+
const { positionX, positionY } = ref.current!.instance.state;
80+
expect(positionX).toBeLessThan(2000);
81+
expect(positionY).toBeLessThan(2000);
82+
});
83+
84+
it("allows panning freely when limitToBounds is false", () => {
85+
const { pan, content } = renderApp({
86+
limitToBounds: false,
87+
});
88+
pan({ x: -200, y: -200 });
89+
expect(content.style.transform).toBe(
90+
"translate(-200px, -200px) scale(1)",
91+
);
92+
});
93+
});
94+
95+
describe("ReactZoomPanPinchProps.centerZoomedOut", () => {
96+
it("defaults to true — content stays centered when zoomed below 1", () => {
97+
const { ref } = renderApp({
98+
centerZoomedOut: true,
99+
minScale: 0.5,
100+
});
101+
ref.current!.setTransform(0, 0, 0.5);
102+
const { positionX, positionY } = ref.current!.instance.state;
103+
expect(positionX).toBeGreaterThanOrEqual(0);
104+
expect(positionY).toBeGreaterThanOrEqual(0);
105+
});
106+
});
107+
108+
describe("ReactZoomPanPinchProps.centerOnInit", () => {
109+
it("centers content in the viewport on mount", async () => {
110+
const { ref } = renderApp({
111+
centerOnInit: true,
112+
limitToBounds: false,
113+
contentHeight: "2000px",
114+
wrapperHeight: "500px",
115+
});
116+
await waitFor(() => {
117+
const { positionX, positionY } = ref.current!.instance.state;
118+
expect(positionX !== 0 || positionY !== 0).toBe(true);
119+
});
120+
});
121+
});
122+
123+
describe("ReactZoomPanPinchProps.disablePadding", () => {
124+
it("prevents overscroll past bounds when true", () => {
125+
const { pan, content } = renderApp({
126+
disablePadding: true,
127+
});
128+
expect(content.style.transform).toBe("translate(0px, 0px) scale(1)");
129+
pan({ x: -100, y: -100 });
130+
expect(content.style.transform).toBe("translate(0px, 0px) scale(1)");
131+
});
132+
133+
it("allows overscroll (padding) when false", () => {
134+
const { pan, content } = renderApp({
135+
disablePadding: false,
136+
});
137+
pan({ x: -100, y: -100 });
138+
expect(content.style.transform).toBe(
139+
"translate(-100px, -100px) scale(1)",
140+
);
141+
});
142+
});
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { act, fireEvent, waitFor } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
3+
4+
import { renderApp, flushAnimationFrames } from "../../utils";
5+
6+
describe("ReactZoomPanPinchProps callbacks", () => {
7+
describe("onInit", () => {
8+
it("fires on mount with ref", () => {
9+
const onInit = jest.fn();
10+
renderApp({ onInit });
11+
expect(onInit).toHaveBeenCalledTimes(1);
12+
expect(onInit.mock.calls[0][0]).toHaveProperty("instance");
13+
});
14+
});
15+
16+
describe("onPanningStart", () => {
17+
it("fires when panning begins", () => {
18+
const onPanningStart = jest.fn();
19+
const { pan } = renderApp({ onPanningStart });
20+
pan({ x: -50, y: -50 });
21+
expect(onPanningStart).toHaveBeenCalled();
22+
});
23+
});
24+
25+
describe("onPanning", () => {
26+
it("fires during panning", () => {
27+
const onPanning = jest.fn();
28+
const { pan } = renderApp({ onPanning });
29+
pan({ x: -50, y: -50, moveEventCount: 3 });
30+
expect(onPanning).toHaveBeenCalled();
31+
});
32+
});
33+
34+
describe("onPanningStop", () => {
35+
it("fires when panning ends", () => {
36+
const onPanningStop = jest.fn();
37+
const { pan } = renderApp({ onPanningStop });
38+
pan({ x: -50, y: -50 });
39+
expect(onPanningStop).toHaveBeenCalled();
40+
});
41+
});
42+
43+
describe("onWheelStart", () => {
44+
it("fires when wheel zoom begins", () => {
45+
const onWheelStart = jest.fn();
46+
const { content } = renderApp({ onWheelStart, smooth: false });
47+
userEvent.hover(content);
48+
fireEvent(
49+
content,
50+
new WheelEvent("wheel", { bubbles: true, deltaY: -5 }),
51+
);
52+
expect(onWheelStart).toHaveBeenCalled();
53+
});
54+
});
55+
56+
describe("onWheel", () => {
57+
it("fires on each wheel tick", () => {
58+
const onWheel = jest.fn();
59+
const { content } = renderApp({ onWheel, smooth: false });
60+
userEvent.hover(content);
61+
fireEvent(
62+
content,
63+
new WheelEvent("wheel", { bubbles: true, deltaY: -5 }),
64+
);
65+
expect(onWheel).toHaveBeenCalled();
66+
});
67+
});
68+
69+
describe("onWheelStop", () => {
70+
it("fires after wheel activity stops", async () => {
71+
const onWheelStop = jest.fn();
72+
const { content } = renderApp({ onWheelStop, smooth: false });
73+
userEvent.hover(content);
74+
fireEvent(
75+
content,
76+
new WheelEvent("wheel", { bubbles: true, deltaY: -5 }),
77+
);
78+
await waitFor(() => {
79+
expect(onWheelStop).toHaveBeenCalled();
80+
});
81+
});
82+
});
83+
84+
describe("onPinchStart", () => {
85+
it("fires when pinch gesture starts", () => {
86+
const onPinchStart = jest.fn();
87+
const { pinch } = renderApp({ onPinchStart });
88+
pinch({ value: 1.5, center: [250, 250] });
89+
expect(onPinchStart).toHaveBeenCalled();
90+
});
91+
});
92+
93+
describe("onPinch", () => {
94+
it("fires during pinch gesture", () => {
95+
const onPinch = jest.fn();
96+
const { pinch } = renderApp({ onPinch });
97+
pinch({ value: 1.5, center: [250, 250] });
98+
expect(onPinch).toHaveBeenCalled();
99+
});
100+
});
101+
102+
describe("onPinchStop", () => {
103+
it("fires when pinch gesture ends", () => {
104+
const onPinchStop = jest.fn();
105+
const { pinch } = renderApp({ onPinchStop });
106+
pinch({ value: 1.5, center: [250, 250] });
107+
expect(onPinchStop).toHaveBeenCalled();
108+
});
109+
});
110+
111+
describe("onZoomStart", () => {
112+
it("fires when any zoom starts", () => {
113+
const onZoomStart = jest.fn();
114+
const { content } = renderApp({ onZoomStart, smooth: false });
115+
userEvent.hover(content);
116+
fireEvent(
117+
content,
118+
new WheelEvent("wheel", { bubbles: true, deltaY: -5 }),
119+
);
120+
expect(onZoomStart).toHaveBeenCalled();
121+
});
122+
});
123+
124+
describe("onZoom", () => {
125+
it("fires during zoom", () => {
126+
const onZoom = jest.fn();
127+
const { content } = renderApp({ onZoom, smooth: false });
128+
userEvent.hover(content);
129+
fireEvent(
130+
content,
131+
new WheelEvent("wheel", { bubbles: true, deltaY: -5 }),
132+
);
133+
expect(onZoom).toHaveBeenCalled();
134+
});
135+
});
136+
137+
describe("onZoomStop", () => {
138+
it("fires after zoom ends", async () => {
139+
const onZoomStop = jest.fn();
140+
const { content } = renderApp({ onZoomStop, smooth: false });
141+
userEvent.hover(content);
142+
fireEvent(
143+
content,
144+
new WheelEvent("wheel", { bubbles: true, deltaY: -5 }),
145+
);
146+
await waitFor(() => {
147+
expect(onZoomStop).toHaveBeenCalled();
148+
});
149+
});
150+
});
151+
152+
describe("onTransform", () => {
153+
it("fires on every transform change", () => {
154+
const onTransform = jest.fn();
155+
const { pan } = renderApp({ onTransform });
156+
pan({ x: -50, y: -50 });
157+
expect(onTransform).toHaveBeenCalled();
158+
const lastCall = onTransform.mock.calls[onTransform.mock.calls.length - 1];
159+
expect(lastCall[1]).toHaveProperty("scale");
160+
expect(lastCall[1]).toHaveProperty("positionX");
161+
expect(lastCall[1]).toHaveProperty("positionY");
162+
});
163+
});
164+
});

0 commit comments

Comments
 (0)