Skip to content

Commit 33d4bab

Browse files
committed
add tests with mulitple thresholds
1 parent 2b5e7f0 commit 33d4bab

File tree

2 files changed

+125
-3
lines changed

2 files changed

+125
-3
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ The `useOnInView` hook provides a more direct alternative to `useInView`. It tak
257257
258258
Key differences from `useInView`:
259259
- **No re-renders** - This hook doesn't update any state, making it ideal for performance-critical scenarios
260-
- **Direct element access** - Your callback receives the actual DOM element and the IntersectionObserverEntry
260+
- **Direct element access** - Your callback receives the actual IntersectionObserverEntry with the `target` element
261261
- **Optional cleanup** - Return a function from your callback to run when the element leaves the viewport
262262
- **Similar options** - Accepts all the same [options](#options) as `useInView` except `onChange` and `initialInView`
263263

src/__tests__/useOnInView.test.tsx

+124-2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,44 @@ const OnInViewChangedComponentWithoutClenaup = ({
8787
);
8888
};
8989

90+
const ThresholdTriggerComponent = ({
91+
options,
92+
}: {
93+
options?: IntersectionEffectOptions;
94+
}) => {
95+
const [triggerCount, setTriggerCount] = React.useState(0);
96+
const [lastRatio, setLastRatio] = React.useState<number | null>(null);
97+
const [triggeredThresholds, setTriggeredThresholds] = React.useState<
98+
number[]
99+
>([]);
100+
101+
const inViewRef = useOnInView((entry) => {
102+
setTriggerCount((prev) => prev + 1);
103+
setLastRatio(entry.intersectionRatio);
104+
105+
// Add this ratio to our list of triggered thresholds
106+
setTriggeredThresholds((prev) => [...prev, entry.intersectionRatio]);
107+
108+
return (exitEntry) => {
109+
if (exitEntry) {
110+
setLastRatio(exitEntry.intersectionRatio);
111+
}
112+
};
113+
}, options);
114+
115+
return (
116+
<div
117+
data-testid="threshold-trigger"
118+
ref={inViewRef}
119+
data-trigger-count={triggerCount}
120+
data-last-ratio={lastRatio !== null ? lastRatio.toFixed(2) : "null"}
121+
data-triggered-thresholds={JSON.stringify(triggeredThresholds)}
122+
>
123+
Tracking thresholds
124+
</div>
125+
);
126+
};
127+
90128
test("should create a hook with useOnInView", () => {
91129
const { getByTestId } = render(<OnInViewChangedComponent />);
92130
const wrapper = getByTestId("wrapper");
@@ -172,7 +210,7 @@ test("should call callback with trigger: leave and triggerOnce is true", () => {
172210
const wrapper = getByTestId("wrapper");
173211

174212
mockAllIsIntersecting(true);
175-
// initialInView should have triggered the callback once
213+
// the callback should not be called as it is triggered on leave
176214
expect(wrapper.getAttribute("data-call-count")).toBe("0");
177215

178216
mockAllIsIntersecting(false);
@@ -338,7 +376,7 @@ test("should pass the element to the callback", () => {
338376

339377
const ElementTestComponent = () => {
340378
const inViewRef = useOnInView((entry) => {
341-
capturedElement = entry?.target;
379+
capturedElement = entry.target;
342380
return undefined;
343381
});
344382

@@ -351,3 +389,87 @@ test("should pass the element to the callback", () => {
351389

352390
expect(capturedElement).toBe(element);
353391
});
392+
393+
test("should track which threshold triggered the visibility change", () => {
394+
// Using multiple specific thresholds
395+
const { getByTestId } = render(
396+
<ThresholdTriggerComponent options={{ threshold: [0.25, 0.5, 0.75] }} />,
397+
);
398+
const element = getByTestId("threshold-trigger");
399+
400+
// Initially not in view
401+
expect(element.getAttribute("data-trigger-count")).toBe("0");
402+
403+
// Trigger at exactly the first threshold (0.25)
404+
mockAllIsIntersecting(0.25);
405+
expect(element.getAttribute("data-trigger-count")).toBe("1");
406+
expect(element.getAttribute("data-last-ratio")).toBe("0.25");
407+
408+
// Go out of view
409+
mockAllIsIntersecting(0);
410+
411+
// Trigger at exactly the second threshold (0.5)
412+
mockAllIsIntersecting(0.5);
413+
expect(element.getAttribute("data-trigger-count")).toBe("2");
414+
expect(element.getAttribute("data-last-ratio")).toBe("0.50");
415+
416+
// Go out of view
417+
mockAllIsIntersecting(0);
418+
419+
// Trigger at exactly the third threshold (0.75)
420+
mockAllIsIntersecting(0.75);
421+
expect(element.getAttribute("data-trigger-count")).toBe("3");
422+
expect(element.getAttribute("data-last-ratio")).toBe("0.75");
423+
424+
// Check all triggered thresholds were recorded
425+
const triggeredThresholds = JSON.parse(
426+
element.getAttribute("data-triggered-thresholds") || "[]",
427+
);
428+
expect(triggeredThresholds).toContain(0.25);
429+
expect(triggeredThresholds).toContain(0.5);
430+
expect(triggeredThresholds).toContain(0.75);
431+
});
432+
433+
test("should track thresholds when crossing multiple in a single update", () => {
434+
// Using multiple specific thresholds
435+
const { getByTestId } = render(
436+
<ThresholdTriggerComponent options={{ threshold: [0.2, 0.4, 0.6, 0.8] }} />,
437+
);
438+
const element = getByTestId("threshold-trigger");
439+
440+
// Initially not in view
441+
expect(element.getAttribute("data-trigger-count")).toBe("0");
442+
443+
// Jump straight to 0.7 (crosses 0.2, 0.4, 0.6 thresholds)
444+
// The IntersectionObserver will still only call the callback once
445+
// with the highest threshold that was crossed
446+
mockAllIsIntersecting(0.7);
447+
expect(element.getAttribute("data-trigger-count")).toBe("1");
448+
expect(element.getAttribute("data-last-ratio")).toBe("0.60");
449+
450+
// Go out of view
451+
mockAllIsIntersecting(0);
452+
453+
// Jump to full visibility
454+
mockAllIsIntersecting(1.0);
455+
expect(element.getAttribute("data-trigger-count")).toBe("2");
456+
expect(element.getAttribute("data-last-ratio")).toBe("0.80");
457+
});
458+
459+
test("should track thresholds when trigger is set to leave", () => {
460+
// Using multiple specific thresholds with trigger: leave
461+
const { getByTestId } = render(
462+
<ThresholdTriggerComponent
463+
options={{
464+
threshold: [0.25, 0.5, 0.75],
465+
trigger: "leave",
466+
}}
467+
/>,
468+
);
469+
const element = getByTestId("threshold-trigger");
470+
471+
// Make element 30% visible - above first threshold, should call cleanup
472+
mockAllIsIntersecting(0);
473+
expect(element.getAttribute("data-trigger-count")).toBe("1");
474+
expect(element.getAttribute("data-last-ratio")).toBe("0.00");
475+
});

0 commit comments

Comments
 (0)