import React , { useRef , useState , useCallback } from "react" ; import useResizeObserver from "../" ; import { render , cleanup } from "@testing-library/react" ; import useRenderTrigger from "./utils/useRenderTrigger" ; import awaitNextFrame from "./utils/awaitNextFrame" ; import createController from "./utils/createController" ; import useMergedCallbackRef from "./utils/useMergedCallbackRef" ; import { ObservedSize , supports } from "./utils" ; afterEach ( ( ) => { cleanup ( ) ; } ) ; describe ( "Testing Lib: Basics" , ( ) => { it ( "should measure the right sizes" , async ( ) => { const controller = createController ( ) ; const Test = ( ) => { const { ref , width = 0 , height = 0 , } = useResizeObserver < HTMLDivElement > ( ) ; const mergedCallbackRef = useMergedCallbackRef ( ref , ( element ) => { controller . provideSetSizeFunction ( element ) ; } ) ; controller . incrementRenderCount ( ) ; controller . reportMeasuredSize ( { width , height } ) ; return < div ref = { mergedCallbackRef } />; } ; render ( < Test />); controller . assertMeasuredSize ( { width : 0 , height : 0 } ) ; controller . assertRenderCount ( 1 ) ; await controller . setSize ( { width : 100 , height : 200 } ) ; controller . assertMeasuredSize ( { width : 100 , height : 200 } ) ; controller . assertRenderCount ( 2 ) ; } ) ; } ) ; describe ( "Testing Lib: Resize Observer Instance Counting Block" , ( ) => { let resizeObserverInstanceCount = 0 ; let resizeObserverObserveCount = 0 ; let resizeObserverUnobserveCount = 0 ; const NativeResizeObserver = ( window as any ) . ResizeObserver ; beforeAll ( ( ) => { ( window as any ) . ResizeObserver = function PatchedResizeObserver ( cb ) { resizeObserverInstanceCount ++ ; const ro = new NativeResizeObserver ( cb ) as ResizeObserver ; return { observe : ( element ) => { resizeObserverObserveCount ++ ; return ro . observe ( element ) ; } , unobserve : ( element ) => { resizeObserverUnobserveCount ++ ; return ro . unobserve ( element ) ; } , } ; } ; } ) ; beforeEach ( ( ) => { resizeObserverInstanceCount = 0 ; resizeObserverObserveCount = 0 ; resizeObserverUnobserveCount = 0 ; } ) ; afterAll ( ( ) => { try { ( window as any ) . ResizeObserver = NativeResizeObserver ; } catch ( error ) { } } ) ; it ( "should use a single ResizeObserver instance even if the onResize callback is not memoised" , async ( ) => { const controller = createController ( ) ; const Test = ( ) => { const { ref } = useResizeObserver < HTMLDivElement > ( { onResize : ( ) => { } , } ) ; controller . triggerRender = useRenderTrigger ( ) ; return < div ref = { ref } />; } ; render ( < Test />); await awaitNextFrame ( ) ; await controller . triggerRender ( ) ; expect ( resizeObserverInstanceCount ) . toBe ( 1 ) ; expect ( resizeObserverObserveCount ) . toBe ( 1 ) ; expect ( resizeObserverUnobserveCount ) . toBe ( 0 ) ; } ) ; it ( "should not create a new RO instance if the hook is the same and the observed element changes" , async ( ) => { const Test = ( { observeNewElement = false } ) => { const customRef = useRef < HTMLDivElement > ( null ) ; const { ref } = useResizeObserver < HTMLDivElement > ( { ref : observeNewElement ? customRef : null , } ) ; if ( observeNewElement ) { return < span ref = { customRef } />; } return < div ref = { ref } />; } ; const { rerender } = render ( < Test />); await awaitNextFrame ( ) ; expect ( resizeObserverInstanceCount ) . toBe ( 1 ) ; expect ( resizeObserverObserveCount ) . toBe ( 1 ) ; expect ( resizeObserverUnobserveCount ) . toBe ( 0 ) ; rerender ( < Test observeNewElement = { true } / > ) ; await awaitNextFrame ( ) ; expect ( resizeObserverInstanceCount ) . toBe ( 1 ) ; expect ( resizeObserverObserveCount ) . toBe ( 2 ) ; expect ( resizeObserverUnobserveCount ) . toBe ( 1 ) ; } ) ; it ( "should not create a ResizeObserver instance until there's an actual element present to be measured" , async ( ) => { let renderCount = 0 ; let measuredWidth ; let measuredHeight ; const Test = ( { doMeasure } ) => { const ref = useRef < HTMLDivElement > ( null ) ; const { width , height } = useResizeObserver ( { ref : doMeasure ? ref : null , } ) ; renderCount ++ ; measuredWidth = width ; measuredHeight = height ; return < div ref = { ref } style = { { width : 100 , height : 200 } } />; } ; const { rerender } = render ( < Test doMeasure = { false } / > ) ; expect ( resizeObserverInstanceCount ) . toBe ( 0 ) ; expect ( renderCount ) . toBe ( 1 ) ; expect ( measuredWidth ) . toBe ( undefined ) ; expect ( measuredHeight ) . toBe ( undefined ) ; rerender ( < Test doMeasure = { true } / > ) ; await awaitNextFrame ( ) ; expect ( resizeObserverInstanceCount ) . toBe ( 1 ) ; expect ( renderCount ) . toBe ( 3 ) ; expect ( measuredWidth ) . toBe ( 100 ) ; expect ( measuredHeight ) . toBe ( 200 ) ; } ) ; it ( "should work with refs even if the ref value is filled by react later, with a delayed mount" , async ( ) => { const controller = createController ( ) ; const Test = ( { mount = false } ) => { const { ref , width , height } = useResizeObserver < HTMLDivElement > ( ) ; controller . triggerRender = useRenderTrigger ( ) ; controller . reportMeasuredSize ( { width , height } ) ; if ( ! mount ) { return null ; } return < div ref = { ref } style = { { width : 100 , height : 200 } } />; } ; const { rerender } = render ( < Test />); controller . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await awaitNextFrame ( ) ; controller . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await awaitNextFrame ( ) ; rerender ( < Test mount = { true } / > ) ; controller . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await awaitNextFrame ( ) ; await controller . triggerRender ( ) ; controller . assertMeasuredSize ( { width : 100 , height : 200 } ) ; } ) ; it ( "should pick up on delayed mounts" , async ( ) => { const controller = createController ( ) ; const Test = ( { mount = false } ) => { const { ref , width , height } = useResizeObserver < HTMLDivElement > ( ) ; controller . reportMeasuredSize ( { width , height } ) ; if ( ! mount ) { return null ; } return < div ref = { ref } style = { { width : 100 , height : 200 } } />; } ; const { rerender } = render ( < Test />); controller . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await awaitNextFrame ( ) ; controller . assertMeasuredSize ( { width : undefined , height : undefined } ) ; rerender ( < Test mount = { true } / > ) ; await awaitNextFrame ( ) ; controller . assertMeasuredSize ( { width : 100 , height : 200 } ) ; } ) ; it ( "should work on a normal mount" , async ( ) => { const controller = createController ( ) ; const Test = ( ) => { const { ref , width , height } = useResizeObserver < HTMLDivElement > ( ) ; controller . reportMeasuredSize ( { width , height } ) ; return < div ref = { ref } style = { { width : 100 , height : 200 } } />; } ; render ( < Test />); controller . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await awaitNextFrame ( ) ; controller . assertMeasuredSize ( { width : 100 , height : 200 } ) ; } ) ; it ( "should work with a regular element as the 'custom ref' too" , async ( ) => { const controller = createController ( ) ; const Test = ( ) => { const [ element , setElement ] = useState < HTMLDivElement | null > ( null ) ; const { width , height } = useResizeObserver < HTMLDivElement > ( { ref : element , } ) ; const receiveElement = useCallback ( ( element ) => { setElement ( element ) ; } , [ ] ) ; controller . reportMeasuredSize ( { width , height } ) ; return < div ref = { receiveElement } style = { { width : 100 , height : 200 } } />; } ; render ( < Test />); controller . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await awaitNextFrame ( ) ; controller . assertMeasuredSize ( { width : 100 , height : 200 } ) ; } ) ; it ( "should support switching back-and-forth between box types" , async ( ) => { const c1 = createController ( ) ; type Controller = { setBox : ( box : ResizeObserverBoxOptions ) => Promise < void > ; } ; const c2 = { } as Controller ; const Test = ( ) => { const [ box , setBox ] = useState < ResizeObserverBoxOptions > ( "border-box" ) ; c2 . setBox = useCallback ( async ( box ) => { setBox ( box ) ; await awaitNextFrame ( ) ; } , [ ] ) ; const { ref , width , height } = useResizeObserver < HTMLDivElement > ( { box } ) ; const mergedCallbackRef = useMergedCallbackRef ( ref , ( element ) => { c1 . provideSetSizeFunction ( element ) ; } ) ; c1 . incrementRenderCount ( ) ; c1 . reportMeasuredSize ( { width , height } ) ; return ( < div ref = { mergedCallbackRef } style = { { padding : "10px 20px" , border : "1px solid" } } />); } ; render ( < Test />); c1 . assertMeasuredSize ( { width : undefined , height : undefined } ) ; c1 . assertRenderCount ( 1 ) ; await c1 . setSize ( { width : 100 , height : 200 } ) ; if ( supports . borderBox ) { c1 . assertRenderCount ( 2 ) ; c1 . assertMeasuredSize ( { width : 142 , height : 222 } ) ; } else { c1 . assertRenderCount ( 1 ) ; c1 . assertMeasuredSize ( { width : undefined , height : undefined } ) ; } await c2 . setBox ( "content-box" ) ; c1 . assertMeasuredSize ( { width : 100 , height : 200 } ) ; if ( supports . borderBox ) { c1 . assertRenderCount ( 4 ) ; } else { c1 . assertRenderCount ( 3 ) ; } await c2 . setBox ( "border-box" ) ; if ( supports . borderBox ) { c1 . assertRenderCount ( 6 ) ; c1 . assertMeasuredSize ( { width : 142 , height : 222 } ) ; } else { c1 . assertRenderCount ( 5 ) ; c1 . assertMeasuredSize ( { width : undefined , height : undefined } ) ; } } ) ; it ( "should be able to measure device pixel content box in supporting browsers" , async ( ) => { const c1 = createController ( ) ; type Controller = { setZoom : ( zoom : number ) => Promise < void > ; } ; const c2 = { } as Controller ; const Test = ( ) => { const { ref , width , height } = useResizeObserver < HTMLDivElement > ( { box : "device-pixel-content-box" , } ) ; const localRef = useRef < HTMLDivElement | null > ( null ) ; c2 . setZoom = useCallback ( async ( zoom ) => { if ( localRef . current ) { localRef . current . style . zoom = String ( zoom ) ; await awaitNextFrame ( ) ; } } , [ ] ) ; const mergedCallbackRef = useMergedCallbackRef ( ref , ( element ) => { localRef . current = element ; if ( localRef . current ) { window . s = localRef . current . style ; } c1 . provideSetSizeFunction ( element ) ; } ) ; c1 . incrementRenderCount ( ) ; c1 . reportMeasuredSize ( { width , height } ) ; return < div ref = { mergedCallbackRef } />; } ; render ( < Test />); c1 . assertRenderCount ( 1 ) ; c1 . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await c1 . setSize ( { width : 100 , height : 200 } ) ; if ( supports . devicePixelContentBoxSize ) { c1 . assertRenderCount ( 2 ) ; c1 . assertMeasuredSize ( { width : Math . round ( 100 * devicePixelRatio ) , height : Math . round ( 200 * devicePixelRatio ) , } ) ; } else { c1 . assertRenderCount ( 1 ) ; c1 . assertMeasuredSize ( { width : undefined , height : undefined } ) ; } } ) ; it ( "should not report repeated values with the onResize callback" , async ( ) => { const c = createController ( ) ; const Test = ( ) => { const [ size , setSize ] = useState < ObservedSize > ( { width : undefined , height : undefined , } ) ; const { ref } = useResizeObserver < HTMLDivElement > ( { onResize : setSize } ) ; const mergedCallbackRef = useMergedCallbackRef ( ref , ( element ) => { c . provideSetSizeFunction ( element ) ; } ) ; c . incrementRenderCount ( ) ; c . reportMeasuredSize ( size ) ; return < div ref = { mergedCallbackRef } />; } ; render ( < Test />); c . assertRenderCount ( 1 ) ; c . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await c . setSize ( { width : 100 , height : 200 } ) ; c . assertRenderCount ( 2 ) ; c . assertMeasuredSize ( { width : 100 , height : 200 } ) ; await c . setSize ( { width : 100.2 , height : 200.4 } ) ; c . assertRenderCount ( 2 ) ; c . assertMeasuredSize ( { width : 100 , height : 200 } ) ; } ) ; it ( "should accept a custom rounding function, and adapt to function instance changes without unnecessary renders" , async ( ) => { const c1 = createController ( ) ; type Controller = { replaceRoundFunction : ( fn : "multiply" | "unset" ) => void ; } ; const c2 = { } as Controller ; const Test = ( ) => { const [ rounder , setRounder ] = useState < typeof Math . ceil | undefined > ( ( ) => Math . ceil ) ; const { ref , width , height } = useResizeObserver < HTMLDivElement > ( { round : rounder , } ) ; const mergedCallbackRef = useMergedCallbackRef ( ref , ( element ) => { c1 . provideSetSizeFunction ( element ) ; c2 . replaceRoundFunction = async ( fn ) => { setRounder ( ( ) => fn === "multiply" ? ( n ) => Math . round ( n * 2 ) : undefined ) ; await awaitNextFrame ( ) ; } ; } ) ; c1 . incrementRenderCount ( ) ; c1 . reportMeasuredSize ( { width , height } ) ; return < div ref = { mergedCallbackRef } />; } ; render ( < Test />); c1 . assertRenderCount ( 1 ) ; c1 . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await c1 . setSize ( { width : 100.1 , height : 200.1 } ) ; c1 . assertRenderCount ( 2 ) ; c1 . assertMeasuredSize ( { width : 101 , height : 201 } ) ; await c1 . setSize ( { width : 200.2 , height : 300.2 } ) ; c1 . assertRenderCount ( 3 ) ; c1 . assertMeasuredSize ( { width : 201 , height : 301 } ) ; await c2 . replaceRoundFunction ( "multiply" ) ; c1 . assertRenderCount ( 5 ) ; c1 . assertMeasuredSize ( { width : 400 , height : 600 } ) ; await c2 . replaceRoundFunction ( "unset" ) ; c1 . assertRenderCount ( 7 ) ; c1 . assertMeasuredSize ( { width : 200 , height : 300 } ) ; } ) ; it ( "should only re-render with a custom rounding function when it produces a new value" , async ( ) => { const c = createController ( ) ; const rounder = ( n ) => { if ( n < 500 ) { return 0 ; } else if ( n < 1000 ) { return 500 ; } return 1000 ; } ; const Test = ( ) => { const { ref , width , height } = useResizeObserver < HTMLDivElement > ( { round : rounder , } ) ; const mergedCallbackRef = useMergedCallbackRef ( ref , ( element ) => { c . provideSetSizeFunction ( element ) ; } ) ; c . incrementRenderCount ( ) ; c . reportMeasuredSize ( { width , height } ) ; return < div ref = { mergedCallbackRef } />; } ; render ( < Test />); c . assertRenderCount ( 1 ) ; c . assertMeasuredSize ( { width : undefined , height : undefined } ) ; await c . setSize ( { width : 100 , height : 100 } ) ; c . assertRenderCount ( 2 ) ; c . assertMeasuredSize ( { width : 0 , height : 0 } ) ; await c . setSize ( { width : 200 , height : 200 } ) ; c . assertRenderCount ( 2 ) ; c . assertMeasuredSize ( { width : 0 , height : 0 } ) ; await c . setSize ( { width : 600 , height : 600 } ) ; c . assertRenderCount ( 3 ) ; c . assertMeasuredSize ( { width : 500 , height : 500 } ) ; await c . setSize ( { width : 1100 , height : 600 } ) ; c . assertRenderCount ( 4 ) ; c . assertMeasuredSize ( { width : 1000 , height : 500 } ) ; await c . setSize ( { width : 1100 , height : 800 } ) ; c . assertRenderCount ( 4 ) ; c . assertMeasuredSize ( { width : 1000 , height : 500 } ) ; await c . setSize ( { width : 1100 , height : 1100 } ) ; c . assertRenderCount ( 5 ) ; c . assertMeasuredSize ( { width : 1000 , height : 1000 } ) ; } ) ; } ) ;