Skip to content

Commit 721cd61

Browse files
authored
feat: add support for testing threshold values (#417)
This expands the `test-utils` to support defining a `threshold` value when checking for `isIntersecting`. Before you could only give a `boolean`. This should allow you test more complex `threshold` based scenarios.
1 parent efef402 commit 721cd61

File tree

3 files changed

+54
-15
lines changed

3 files changed

+54
-15
lines changed

README.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ argument for the hooks.
153153
| **delay** 🧪 | number | undefined | false | A number indicating the minimum delay in milliseconds between notifications from this observer for a given target. This must be set to at least `100` if `trackVisibility` is `true`. |
154154
| **skip** | boolean | false | false | Skip creating the IntersectionObserver. You can use this to enable and disable the observer as needed. If `skip` is set while `inView`, the current state will still be kept. |
155155
| **triggerOnce** | boolean | false | false | Only trigger the observer once. |
156-
| **initialInView** | boolean | false | false | Set the initial value of the `inView` boolean. This can be used if you expect the element to be in the viewport to start with, and you want to trigger something when it leaves. |
156+
| **initialInView** | boolean | false | false | Set the initial value of the `inView` boolean. This can be used if you expect the element to be in the viewport to start with, and you want to trigger something when it leaves. |
157157

158158
> ⚠️ When passing an array to `threshold`, store the array in a constant to
159159
> avoid the component re-rendering too often. For example:
@@ -266,17 +266,18 @@ You can read more about this on these links:
266266
In order to write meaningful tests, the `IntersectionObserver` needs to be
267267
mocked. If you are writing your tests in Jest, you can use the included
268268
`test-utils.js`. It mocks the `IntersectionObserver`, and includes a few methods
269-
to assist with faking the `inView` state.
269+
to assist with faking the `inView` state. When setting the `isIntersecting`
270+
value you can pass either a `boolean` value or a threshold between `0` and `1`.
270271
271272
### `test-utils.js`
272273
273274
Import the methods from `react-intersection-observer/test-utils`.
274275
275-
**`mockAllIsIntersecting(isIntersecting:boolean)`**
276-
Set the `isIntersecting` on all current IntersectionObserver instances.
276+
**`mockAllIsIntersecting(isIntersecting:boolean | number)`**
277+
Set `isIntersecting` on all current IntersectionObserver instances.
277278
278-
**`mockIsIntersecting(element:Element, isIntersecting:boolean)`**
279-
Set the `isIntersecting` for the IntersectionObserver of a specific element.
279+
**`mockIsIntersecting(element:Element, isIntersecting:boolean | number)`**
280+
Set `isIntersecting` for the IntersectionObserver of a specific element.
280281
281282
**`intersectionMockInstance(element:Element): IntersectionObserver`**
282283
Call the `intersectionMockInstance` method with an element, to get the (mocked)
@@ -287,7 +288,7 @@ Call the `intersectionMockInstance` method with an element, to get the (mocked)
287288
288289
```js
289290
import React from 'react';
290-
import { render } from 'react-testing-library';
291+
import { screen, render } from 'react-testing-library';
291292
import { useInView } from 'react-intersection-observer';
292293
import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils';
293294

@@ -297,11 +298,22 @@ const HookComponent = ({ options }) => {
297298
};
298299

299300
test('should create a hook inView', () => {
300-
const { getByText } = render(<HookComponent />);
301+
render(<HookComponent />);
301302

302303
// This causes all (existing) IntersectionObservers to be set as intersecting
303304
mockAllIsIntersecting(true);
304-
getByText('true');
305+
screen.getByText('true');
306+
});
307+
308+
test('should create a hook inView with threshold', () => {
309+
render(<HookComponent options={{ threshold: 0.3 }} />);
310+
311+
mockAllIsIntersecting(0.1);
312+
screen.getByText('false');
313+
314+
// Once the threshold has been passed, it will trigger inView.
315+
mockAllIsIntersecting(0.3);
316+
screen.getByText('true');
305317
});
306318
```
307319

src/__tests__/hooks.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ test('should create a hook inView', () => {
6767
getByText('true');
6868
});
6969

70+
test('should mock thresholds', () => {
71+
render(<HookComponent options={{ threshold: [0.5, 1] }} />);
72+
mockAllIsIntersecting(0.2);
73+
screen.getByText('false');
74+
mockAllIsIntersecting(0.5);
75+
screen.getByText('true');
76+
mockAllIsIntersecting(1);
77+
screen.getByText('true');
78+
});
79+
7080
test('should create a hook with initialInView', () => {
7181
const { getByText } = render(
7282
<HookComponent options={{ initialInView: true }} />,

src/test-utils.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,28 @@ afterEach(() => {
5252

5353
function triggerIntersection(
5454
elements: Element[],
55-
isIntersecting: boolean,
55+
trigger: boolean | number,
5656
observer: IntersectionObserver,
5757
item: Item,
5858
) {
5959
const entries: IntersectionObserverEntry[] = [];
60+
61+
const isIntersecting =
62+
typeof trigger === 'number'
63+
? observer.thresholds.some((threshold) => trigger >= threshold)
64+
: trigger;
65+
66+
const ratio =
67+
typeof trigger === 'number'
68+
? observer.thresholds.find((threshold) => trigger >= threshold) ?? 0
69+
: trigger
70+
? 1
71+
: 0;
72+
6073
elements.forEach((element) => {
6174
entries.push({
6275
boundingClientRect: element.getBoundingClientRect(),
63-
intersectionRatio: isIntersecting ? 1 : 0,
76+
intersectionRatio: ratio,
6477
intersectionRect: isIntersecting
6578
? element.getBoundingClientRect()
6679
: {
@@ -88,9 +101,9 @@ function triggerIntersection(
88101

89102
/**
90103
* Set the `isIntersecting` on all current IntersectionObserver instances
91-
* @param isIntersecting {boolean}
104+
* @param isIntersecting {boolean | number}
92105
*/
93-
export function mockAllIsIntersecting(isIntersecting: boolean) {
106+
export function mockAllIsIntersecting(isIntersecting: boolean | number) {
94107
for (let [observer, item] of observers) {
95108
triggerIntersection(
96109
Array.from(item.elements),
@@ -103,10 +116,14 @@ export function mockAllIsIntersecting(isIntersecting: boolean) {
103116

104117
/**
105118
* Set the `isIntersecting` for the IntersectionObserver of a specific element.
119+
*
106120
* @param element {Element}
107-
* @param isIntersecting {boolean}
121+
* @param isIntersecting {boolean | number}
108122
*/
109-
export function mockIsIntersecting(element: Element, isIntersecting: boolean) {
123+
export function mockIsIntersecting(
124+
element: Element,
125+
isIntersecting: boolean | number,
126+
) {
110127
const observer = intersectionMockInstance(element);
111128
if (!observer) {
112129
throw new Error(

0 commit comments

Comments
 (0)