Skip to content

Commit 55f0d4e

Browse files
authored
Merge pull request #39 from trurl-master/update-ro-2
Refactor resize observer, add implicit elements
2 parents a401d07 + 818a15c commit 55f0d4e

File tree

15 files changed

+1055
-171
lines changed

15 files changed

+1055
-171
lines changed

README.md

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ A set of tools for emulating browser behavior in jsdom environment
1515
## Installation
1616

1717
```sh
18-
npm i --D jsdom-testing-mocks
18+
npm i -D jsdom-testing-mocks
1919
```
2020

2121
or
@@ -24,6 +24,13 @@ or
2424
yarn add -D jsdom-testing-mocks
2525
```
2626

27+
## Mocks
28+
29+
[matchMedia](#mock-viewport),
30+
[Intersection Observer](#mock-intersectionobserver),
31+
[Resize Observer](#mock-resizeobserver),
32+
[Web Animations API](#mock-web-animations-api)
33+
2734
## Mock viewport
2835

2936
Mocks `matchMedia`, allows testing of component's behavior depending on the viewport description (supports all of the [Media Features](http://www.w3.org/TR/css3-mediaqueries/#media1)). `mockViewport` must be called before rendering the component
@@ -154,17 +161,12 @@ Triggers all IntersectionObservers for each of the observed nodes
154161

155162
## Mock ResizeObserver
156163

157-
Provides a way of triggering resize observer events. It's up to you to mock elements' sizes. If your component uses `contentRect` provided by the callback, you must mock element's `getBoundingClientRect` (for exemple using a helper function `mockElementBoundingClientRect` provided by the lib)
158-
159-
_Currently the mock doesn't take into account multi-column layouts, so `borderBoxSize` and `contentBoxSize` will contain only one full-sized item_
164+
Mocks `ResizeObserver` class. Resize callbacks are triggered manually using `resize` method returned by the mock. Elements' size must not be 0 (at least on one axis) for the element to appear in the list of callback entries (you can mock the size using [`mockElementSize`](#mockelementsizeelement-htmlelement-size-size) or `mockElementBoundingClientRect`)
160165

161166
Example, using `React Testing Library`:
162167

163168
```jsx
164-
import {
165-
mockResizeObserver,
166-
mockElementBoundingClientRect,
167-
} from 'jsdom-testing-mocks';
169+
import { mockResizeObserver } from 'jsdom-testing-mocks';
168170

169171
const DivWithSize = () => {
170172
const [size, setSize] = useState({ width: 0, height: 0 });
@@ -201,31 +203,94 @@ it('prints the size of the div', () => {
201203

202204
expect(screen.getByText('0 x 0')).toBeInTheDocument();
203205

204-
mockElementBoundingClientRect(theDiv, { width: 300, height: 200 });
206+
resizeObserver.mockElementSize(theDiv, {
207+
contentBoxSize: { inlineSize: 300, blockSize: 200 },
208+
});
205209

206210
act(() => {
207-
resizeObserver.resize(theDiv);
211+
// on the first run you don't have to pass the element,
212+
// it will be included in the list of entries automatically
213+
// because of the call to .observe
214+
resizeObserver.resize();
208215
});
209216

210217
expect(screen.getByText('300 x 200')).toBeInTheDocument();
211218

212-
mockElementBoundingClientRect(theDiv, { width: 200, height: 500 });
219+
resizeObserver.mockElementSize(theDiv, {
220+
contentBoxSize: { inlineSize: 200, blockSize: 500 },
221+
});
213222

214223
act(() => {
224+
// on subsequent calls to `resize` you have to include it
225+
// explicitly, unless observe has been called on it again
215226
resizeObserver.resize(theDiv);
216227
});
217228

218229
expect(screen.getByText('200 x 500')).toBeInTheDocument();
219230
});
220231
```
221232

233+
### Caveats
234+
235+
#### Triggering the callback on observe
236+
237+
Although the mock doesn't call the resize callback on its own, it keeps track of all the cases when it should be implicitly called (like when the element first begins being observed), and it auto-adds them to the list of elements when `resize` is called. You can disable this in `ResizeOptions`
238+
239+
#### Mocking element's size
240+
241+
The mock uses the size provided by `mockElementSize` if present and fallbacks to `getBoundingClientRect` (that you can mock using `mockElementBoundingClientRect`). The issue with `getBoundingClientRect` however is that in the real world the value it returns takes CSS Transforms into account, while the values returned in the observer callback don't. It doesn't really matter because it is you who mocks sizes, but for consistency it is preferred that you use `mockElementSize`
242+
222243
### API
223244

224-
`mockResizeObserver` returns an object, that has one method:
245+
`mockResizeObserver` returns an object, that has several methods:
246+
247+
#### .resize(elements?: HTMLElement | HTMLElement[], options: ResizeOptions)
248+
249+
Triggers all resize observer callbacks for all observers that observe the passed elements. Some elements are implicitly resized by the Resize Observer itself, for example when they first attached using `observe`. This mock doesn't call the callback by itself. Instead, it adds them to the list of `entries` when the next `resize` is called (it happens only once per `observe` per element).
250+
251+
In this example the resize callback will be triggered with all observed elements from within `TestedComponent`:
252+
253+
```jsx
254+
// a component that begins to observe elements in a useEffect
255+
render(<TestedComponent />);
256+
257+
// ...don't forget to mock sizes
258+
259+
act(() => {
260+
// triggers the `resize` callback with the elements for which `observe` has been called
261+
resizeObserver.resize();
262+
});
263+
```
264+
265+
##### ResizeOptions.ignoreImplicit (`false` by default)
266+
267+
If `true`, do not include imlicit elements in the resize callback entries array
268+
269+
#### .mockElementSize(element: HTMLElement, size: Size)
270+
271+
Mocks `element`'s size only for the ResizeObserver. `size` accepts 2 properties: `contentBoxSize` and `borderBoxSize` they're both similar to what you see in the ResizeObserver's callback entry. At least one of them must be present (if the other isn't it is set to be equal to the one present), and the other entry properties are derived from these two (and `window.devicePixelRatio`).
272+
273+
Example:
274+
275+
```jsx
276+
mockElementSize(myDiv, {
277+
// both contentBoxSize and borderBoxSize accept plain objects instead of arrays
278+
contentBoxSize: { inlineSize: 400, blockSize: 200 },
279+
});
280+
281+
mockElementSize(myOtherDiv, {
282+
// only one dimension is required, the other one will be assumed to be 0
283+
borderBoxSize: { inlineSize: 200 },
284+
});
285+
```
286+
287+
#### .getObservers(element?: HTMLElement)
288+
289+
Returns all observers (observing the `element` if passed)
225290

226-
#### .resize(elements: HTMLElement | HTMLElement[])
291+
#### .getObservedElements(observer?: ResizeObserver)
227292

228-
Triggers all resize observer callbacks for all observers that observe the passed elements
293+
Returns all observed elements (of the `observer` if passed)
229294

230295
## Mock Web Animations API
231296

examples/src/components/resize-observer/print-my-size/PrintMySize.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,40 @@ import { useState, useEffect, useRef } from 'react';
33
const PrintMySize = () => {
44
const ref1 = useRef<HTMLDivElement>(null);
55
const ref2 = useRef<HTMLDivElement>(null);
6-
const [sizes, setSizes] = useState<{ width: number; height: number }[]>([]);
6+
const ref3 = useRef<HTMLDivElement>(null);
7+
const [sizes, setSizes] = useState<
8+
Map<HTMLElement, { width: number; height: number }>
9+
>(new Map());
710

811
useEffect(() => {
9-
if (!ref1.current || !ref2.current) {
12+
if (!ref1.current || !ref2.current || !ref3.current) {
1013
return;
1114
}
1215

1316
const observer = new ResizeObserver((entries) => {
14-
setSizes(entries.map((entry) => entry.contentRect));
17+
setSizes(
18+
new Map(
19+
entries.map((entry) => [
20+
entry.target as HTMLElement,
21+
entry.contentRect,
22+
])
23+
)
24+
);
1525
});
1626

1727
observer.observe(ref1.current);
1828
observer.observe(ref2.current);
29+
observer.observe(ref3.current);
1930

2031
return () => {
2132
observer.disconnect();
2233
};
2334
}, []);
2435

36+
const size1 = ref1.current && sizes.get(ref1.current);
37+
const size2 = ref2.current && sizes.get(ref2.current);
38+
const size3 = ref3.current && sizes.get(ref3.current);
39+
2540
return (
2641
<div>
2742
<div
@@ -33,7 +48,7 @@ const PrintMySize = () => {
3348
data-testid="element"
3449
ref={ref1}
3550
>
36-
{sizes[0] && `${sizes[0].width}x${sizes[0].height}`}
51+
{size1 && `${size1.width}x${size1.height}`}
3752
</div>
3853
<div
3954
style={{
@@ -45,7 +60,19 @@ const PrintMySize = () => {
4560
data-testid="element"
4661
ref={ref2}
4762
>
48-
{sizes[1] && `${sizes[1].width}x${sizes[1].height}`}
63+
{size2 && `${size2.width}x${size2.height}`}
64+
</div>
65+
<div
66+
style={{
67+
width: 100,
68+
height: 200,
69+
backgroundColor: 'rgba(255,0,0,0.5)',
70+
marginTop: 20,
71+
}}
72+
data-testid="element"
73+
ref={ref3}
74+
>
75+
{size3 && `${size3.width}x${size3.height}`}
4976
</div>
5077
</div>
5178
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useEffect, useState } from 'react';
2+
3+
type Telementry = {
4+
callback: (entries: ResizeObserverEntry[]) => void;
5+
};
6+
7+
const useResizeObserver = (telemetry: Telementry) => {
8+
const [ro, setRo] = useState<ResizeObserver | null>(null);
9+
10+
useEffect(() => {
11+
const observer = new ResizeObserver((entries) => {
12+
telemetry.callback(entries);
13+
});
14+
15+
setRo(observer);
16+
17+
return () => {
18+
observer.disconnect();
19+
};
20+
}, [telemetry]);
21+
22+
return ro;
23+
};
24+
25+
export default useResizeObserver;

0 commit comments

Comments
 (0)