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 likeoffsetTop
.
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