Home > Back-end >  Set clientHeight and scrollHeight of ref.current in React Enzyme test
Set clientHeight and scrollHeight of ref.current in React Enzyme test

Time:07-21

I have a callback on an element rendered by my function component, and I want to test that this callback is triggered on click. However, this element is only rendered when clientHeight and scrollHeight of a ref are different, but both of these values are returning as 0 (as seen when I log to the console from within useLayoutEffect) during these tests. How can I set the values of ref.current.clientHeight and ref.current.scrollHeight to different nonzero values for the purposes of this test?

Here is my component:

import React, { useState } from "react";
    
const MyComponent = (props: React.PropsWithChildren<MyComponentProps>) => {
    const ref: React.RefObject<HTMLInputElement> = React.CreateRef();
    const { myCallBack } = props;
    const [showItem, setShowItem] = React.useState(false);

    React.useLayoutEffect(() => {
        if (ref.current && ref.current.clientHeight < ref.current.ScrollHeight) {
            setShowItem(true);
        }
    }, [ref]);

    const someAction = (e: React.ChangeEvent<any>) => {
        myCallBack();
    }

    return(
        <div>
            <div ref={ref}>
                <p>Some text...</p>
            </div>
            {showItem && <div><p  onClick={someAction}>Some more text...</p></div>}
        </div>
    );
}

export default MyComponent;

Here is my test:

describe("my tests", (() => { 
  it("my test", async () => {
    let myCallBackMock = jest.fn();
    let wrapper = mount(
        <MyComponent myCallBack={myCallBackMock} />
    );
    wrapper.find(".some-class").simulate("click", { type: "click" });
    expect(myCallBackMock).toHaveBeenCalled();
  });
});

This is the error message I receive:

Method "stimulate" is meant to be run on 1 node. 0 found instead.

CodePudding user response:

Jestjs uses jsdom as its test environment. JSDOM has no layout engine. See Unimplemented parts of the web platform

Layout: the ability to calculate where elements will be visually laid out as a result of CSS, which impacts methods like getBoundingClientRects() or properties like offsetTop.

So it will return zeros for many layout-related properties such as element.clientHeight.

In this case, we have to mock ref and its properties. The clientHeight and scrollHeight properties in your case. So that the showItem state will be set to true on the component mount.

Even though we have to mock React.createRef, we just add properties and values to it in the ref.current setter function and return it rather than a totally fake ref. This means ref.current still references the HTML div element, not the fake JS object.

E.g.

MyComponent.tsx:

import React from 'react';

export interface MyComponentProps {
  myCallBack(): void;
}
export const MyComponent = ({ myCallBack }: React.PropsWithChildren<MyComponentProps>) => {
  const ref: React.RefObject<HTMLInputElement> = React.createRef();
  const [showItem, setShowItem] = React.useState(false);

  React.useLayoutEffect(() => {
    console.log('clientHeight: %s, scrollHeight: %s', ref.current?.clientHeight, ref.current?.scrollHeight);
    if (ref.current && ref.current.clientHeight < ref.current.scrollHeight) {
      setShowItem(true);
    }
  }, []);

  const someAction = (e: React.ChangeEvent<any>) => {
    myCallBack();
  };

  return (
    <div>
      <div ref={ref}>
        <p>Some text...</p>
      </div>
      {showItem && (
        <div>
          <p className="some-class" onClick={someAction}>
            Some more text...
          </p>
        </div>
      )}
    </div>
  );
};

MyComponent.test.tsx:

import { mount } from 'enzyme';
import React from 'react';
import { MyComponent } from './MyComponent';

export function createFCRefMock(props: { [propName: string]: any }) {
  const ref = { current: {} };
  const refKey = Symbol('ref');
  Object.defineProperty(ref, 'current', {
    set(current) {
      // intercept the process of setting ref.current
      if (current) {
        Object.entries(props).forEach(([prop, value]) => {
          Object.defineProperty(current, prop, { value });
        });
      }
      this[refKey] = current;
    },
    get() {
      return this[refKey];
    },
  });
  return ref;
}

describe('my tests', () => {
  it('my test', async () => {
    const myCallBackMock = jest.fn();
    const ref = createFCRefMock({ clientHeight: 10, scrollHeight: 20 });
    jest.spyOn(React, 'createRef').mockReturnValueOnce(ref);
    const wrapper = mount(<MyComponent myCallBack={myCallBackMock} />);
    wrapper.find('.some-class').simulate('click');
    expect(myCallBackMock).toHaveBeenCalled();
  });
});

Test result:

 PASS  stackoverflow/73060890/MyComponent.test.tsx (11.738 s)
  my tests
    ✓ my test (73 ms)

  console.log
    clientHeight: 10, scrollHeight: 20

      at stackoverflow/73060890/MyComponent.tsx:11:13

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |    78.57 |     100 |     100 |                   
 MyComponent.tsx |     100 |    78.57 |     100 |     100 | 11-12             
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.354 s
  • Related