diff --git a/package.json b/package.json index b64c6a8c27..0db04b2e5d 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,9 @@ "coverageDirectory": "coverage", "testMatch": [ "/tests/**/*.test.(ts|tsx)" + ], + "setupFiles": [ + "/tests/_setup.js" ] } } diff --git a/src/useMeasure.ts b/src/useMeasure.ts index 37af79aae8..57d5c6d3fb 100644 --- a/src/useMeasure.ts +++ b/src/useMeasure.ts @@ -1,5 +1,4 @@ import { useState, useMemo } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; export type UseMeasureRect = Pick< @@ -26,7 +25,7 @@ const useMeasure = (): UseMeasureResult => { const observer = useMemo( () => - new ResizeObserver(entries => { + new (window as any).ResizeObserver(entries => { if (entries[0]) { const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect; setRect({ x, y, width, height, top, left, bottom, right }); diff --git a/tests/_setup.js b/tests/_setup.js new file mode 100644 index 0000000000..b4816e4044 --- /dev/null +++ b/tests/_setup.js @@ -0,0 +1,5 @@ +window.ResizeObserver = class ResizeObserver { + constructor() {} + observe() {} + disconnect() {} +}; diff --git a/tests/useMeasure.test.ts b/tests/useMeasure.test.ts index 6d9da805b3..33c6f805d4 100644 --- a/tests/useMeasure.test.ts +++ b/tests/useMeasure.test.ts @@ -1,88 +1,79 @@ -import { act, renderHook } from '@testing-library/react-hooks'; -import useMeasure, { ContentRect } from '../src/useMeasure'; - -interface Entry { - target: HTMLElement; - contentRect: ContentRect; -} - -jest.mock('resize-observer-polyfill', () => { - return class ResizeObserver { - private cb: (entries: Entry[]) => void; - private map: WeakMap; - private targets: HTMLElement[]; - constructor(cb: () => void) { - this.cb = cb; - this.map = new WeakMap(); - this.targets = []; - } - public disconnect() { - this.targets.map(target => { - const originMethod = this.map.get(target); - target.setAttribute = originMethod; - this.map.delete(target); - }); - } - public observe(target: HTMLElement) { - const method = 'setAttribute'; - const originMethod = target[method]; - this.map.set(target, originMethod); - this.targets.push(target); - target[method] = (...args) => { - const [attrName, value] = args; - if (attrName === 'style') { - const rect: DOMRectReadOnly = { - x: 0, - y: 0, - top: 0, - left: 0, - right: 0, - bottom: 0, - width: 0, - height: 0, - } as DOMRectReadOnly; - value.split(';').map(kv => { - const [key, v] = kv.split(':'); - if (['top', 'bottom', 'left', 'right', 'width', 'height'].includes(key)) { - rect[key] = parseInt(v, 10); - } - }); - target.getBoundingClientRect = () => rect; - } - originMethod.apply(target, args); - this.fireCallback(); - }; - } - private fireCallback() { - if (this.cb) { - this.cb( - this.targets.map(target => { - return { - target, - contentRect: target.getBoundingClientRect() as ContentRect, - }; - }) - ); - } +import { renderHook, act } from '@testing-library/react-hooks'; +import useMeasure, { UseMeasureRef } from '../src/useMeasure'; + +it('by default, state defaults every value to -1', () => { + const { result } = renderHook(() => useMeasure()); + + act(() => { + const div = document.createElement('div'); + (result.current[0] as UseMeasureRef)(div); + }); + + expect(result.current[1]).toMatchObject({ + width: -1, + height: -1, + top: -1, + bottom: -1, + left: -1, + right: -1, + }); +}); + +it('synchronously sets up ResizeObserver listener', () => { + let listener: ((rect: any) => void) | undefined = undefined; + (window as any).ResizeObserver = class ResizeObserver { + constructor(ls) { + listener = ls; } + observe() {} + disconnect() {} }; + + const { result } = renderHook(() => useMeasure()); + + act(() => { + const div = document.createElement('div'); + (result.current[0] as UseMeasureRef)(div); + }); + + expect(typeof listener).toBe('function'); }); -it('reacts to changes in size of any of the observed elements', () => { +it('tracks rectangle of a DOM element', () => { + let listener: ((rect: any) => void) | undefined = undefined; + (window as any).ResizeObserver = class ResizeObserver { + constructor(ls) { + listener = ls; + } + observe() {} + disconnect() {} + }; + const { result } = renderHook(() => useMeasure()); - const div = document.createElement('div'); - result.current[0](div); - expect(result.current[1]).toMatchObject({ - width: 0, - height: 0, - top: 0, - bottom: 0, - left: 0, - right: 0, + + act(() => { + const div = document.createElement('div'); + (result.current[0] as UseMeasureRef)(div); + }); + + act(() => { + listener!([{ + contentRect: { + x: 1, + y: 2, + width: 200, + height: 200, + top: 100, + bottom: 0, + left: 100, + right: 0, + } + }]); }); - act(() => div.setAttribute('style', 'width:200px;height:200px;top:100;left:100')); expect(result.current[1]).toMatchObject({ + x: 1, + y: 2, width: 200, height: 200, top: 100, @@ -91,3 +82,97 @@ it('reacts to changes in size of any of the observed elements', () => { right: 0, }); }); + +it('tracks multiple updates', () => { + let listener: ((rect: any) => void) | undefined = undefined; + (window as any).ResizeObserver = class ResizeObserver { + constructor(ls) { + listener = ls; + } + observe() {} + disconnect() {} + }; + + const { result } = renderHook(() => useMeasure()); + + act(() => { + const div = document.createElement('div'); + (result.current[0] as UseMeasureRef)(div); + }); + + act(() => { + listener!([{ + contentRect: { + x: 1, + y: 1, + width: 1, + height: 1, + top: 1, + bottom: 1, + left: 1, + right: 1, + } + }]); + }); + + expect(result.current[1]).toMatchObject({ + x: 1, + y: 1, + width: 1, + height: 1, + top: 1, + bottom: 1, + left: 1, + right: 1, + }); + + act(() => { + listener!([{ + contentRect: { + x: 2, + y: 2, + width: 2, + height: 2, + top: 2, + bottom: 2, + left: 2, + right: 2, + } + }]); + }); + + expect(result.current[1]).toMatchObject({ + x: 2, + y: 2, + width: 2, + height: 2, + top: 2, + bottom: 2, + left: 2, + right: 2, + }); +}); + +it('calls .disconnect() on ResizeObserver when component unmounts', () => { + const disconnect = jest.fn(); + (window as any).ResizeObserver = class ResizeObserver { + constructor() {} + observe() {} + disconnect() { + disconnect(); + } + }; + + const { result, unmount } = renderHook(() => useMeasure()); + + act(() => { + const div = document.createElement('div'); + (result.current[0] as UseMeasureRef)(div); + }); + + expect(disconnect).toHaveBeenCalledTimes(0); + + unmount(); + + expect(disconnect).toHaveBeenCalledTimes(1); +});