I have this custom hook:
import {useEffect, useState} from 'react';
...
const isScrollerAtBottom = (elt: HTMLElement) => {
return (
Math.floor(Number(elt?.scrollHeight) - Number(elt?.scrollTop)) >
Number(elt?.clientHeight)
);
};
export function useScroll(container: HTMLElement): useScrollProps {
console.dir(container, {depth: 12});
const [displayScroller, setDisplayScroller] = useState(true);
const [scrollerTop, setScrollerTop] = useState(0);
const [scrollerLeft, setScrollerLeft] = useState(0);
useEffect(() => {
const containerDimensions = container
? container.getBoundingClientRect()
: null;
console.dir(containerDimensions, {depth: 12});
const left =
(containerDimensions?.x || 0)
(containerDimensions?.width || 0) -
40;
const top = containerDimensions?.height || 0;
setScrollerTop(top);
setScrollerLeft(left);
setDisplayScroller(isScrollerAtBottom(container));
});
...
const handleScroll = (event: React.UIEvent<HTMLElement>) => {
const elt = event.target as HTMLElement;
setDisplayScroller(isScrollerAtBottom(elt));
};
return {
displayScroller,
scrollerTop,
scrollerLeft,
handleScroll,
};
}
And I'd like to test it. My problem is how to mock the HTMLElement as container.
I tried this:
import { JSXElement } from "@babel/types";
import React from 'react';
import { render, renderHook, screen } from "@testing-library/react";
import { useScroll, useScrollProps } from "../../hooks/useScroll";
describe("useScroll", () => {
const container: HTMLElement = document.createElement("div");
container.style.width = "300px";
container.style.height = "800px";
container.style.display = "block";
const content: HTMLElement = document.createElement("p");
content.innerHTML = `... ... ...`;
container.appendChild(content);
test("should return scroller position and display flag", () => {
const { result } = renderHook(() => useScroll(container));
console.log(result.current);
});
});
But the container dimension is not get in the customHook:
console.dir
{
x: 0,
y: 0,
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0
}
If I can get the right dimension of the container mock then I think I can continue to proceed to do some assertions, but I don't know how to get there.
Any idea will be appreciated. Thanks!
POST-EDIT:
What I have seen so far about similar needs is people use to mock such function (getBoundingClientRect
), but if I can't use a mocked HTML element and have the expected calculations for the mocked HTMLElement using the hook I don't think the test will make sense. What I want to test is it returns true/false if the scroll is at the bottom of the container.
Any ideas, comments and/or opinions about this are welcome.
CodePudding user response:
The testing library is more about the expected result rather than the procedure.
Can you create a component inside the test, mount your hook, do whatever you need, and check if the scroll happened?
Or maybe this can help you. https://github.com/testing-library/react-testing-library/issues/671
CodePudding user response:
Yes, I think I see the problem as to why Jest may not be picking up your test React Hook properly
When you use document.createElement("div")
it isn't HTMLElement
being returned as the type but rather an HTMLDivElement
. Likewise, when you use document.createElement("p")
it is HTMLParagraphElement
not HTMLElement
being returned as the type. HTMLElement
is the base Entity that all other JSX.IntrinsicElement
types extend in their interface definitions. You can see this for yourself by plugging the code I've included below into a .ts
file
export type DocumentCreateElementTagNameTandem = {[P in keyof globalThis.HTMLElementTagNameMap]: globalThis.HTMLElementTagNameMap[P]}
I went ahead and set up a dummy function to get intellisense to infer its return type for me, which is included below
export type DocumentCreateElementTagNameTandem = {
[P in keyof globalThis.HTMLElementTagNameMap]: globalThis.HTMLElementTagNameMap[P];
};
export const testingDocumentCreateElementTagNameTandem = (
props: DocumentCreateElementTagNameTandem
): {
a: HTMLAnchorElement;
abbr: HTMLElement;
address: HTMLElement;
area: HTMLAreaElement;
article: HTMLElement;
aside: HTMLElement;
audio: HTMLAudioElement;
b: HTMLElement;
base: HTMLBaseElement;
bdi: HTMLElement;
bdo: HTMLElement;
blockquote: HTMLQuoteElement;
body: HTMLBodyElement;
br: HTMLBRElement;
button: HTMLButtonElement;
canvas: HTMLCanvasElement;
caption: HTMLTableCaptionElement;
cite: HTMLElement;
code: HTMLElement;
col: HTMLTableColElement;
colgroup: HTMLTableColElement;
data: HTMLDataElement;
datalist: HTMLDataListElement;
dd: HTMLElement;
del: HTMLModElement;
details: HTMLDetailsElement;
dfn: HTMLElement;
dialog: HTMLDialogElement;
div: HTMLDivElement;
dl: HTMLDListElement;
dt: HTMLElement;
em: HTMLElement;
embed: HTMLEmbedElement;
fieldset: HTMLFieldSetElement;
figcaption: HTMLElement;
figure: HTMLElement;
footer: HTMLElement;
form: HTMLFormElement;
h1: HTMLHeadingElement;
h2: HTMLHeadingElement;
h3: HTMLHeadingElement;
h4: HTMLHeadingElement;
h5: HTMLHeadingElement;
h6: HTMLHeadingElement;
head: HTMLHeadElement;
header: HTMLElement;
hgroup: HTMLElement;
hr: HTMLHRElement;
html: HTMLHtmlElement;
i: HTMLElement;
iframe: HTMLIFrameElement;
img: HTMLImageElement;
input: HTMLInputElement;
ins: HTMLModElement;
kbd: HTMLElement;
label: HTMLLabelElement;
legend: HTMLLegendElement;
li: HTMLLIElement;
link: HTMLLinkElement;
main: HTMLElement;
map: HTMLMapElement;
mark: HTMLElement;
menu: HTMLMenuElement;
meta: HTMLMetaElement;
meter: HTMLMeterElement;
nav: HTMLElement;
noscript: HTMLElement;
object: HTMLObjectElement;
ol: HTMLOListElement;
optgroup: HTMLOptGroupElement;
option: HTMLOptionElement;
output: HTMLOutputElement;
p: HTMLParagraphElement;
picture: HTMLPictureElement;
pre: HTMLPreElement;
progress: HTMLProgressElement;
q: HTMLQuoteElement;
rp: HTMLElement;
rt: HTMLElement;
ruby: HTMLElement;
s: HTMLElement;
samp: HTMLElement;
script: HTMLScriptElement;
section: HTMLElement;
select: HTMLSelectElement;
slot: HTMLSlotElement;
small: HTMLElement;
source: HTMLSourceElement;
span: HTMLSpanElement;
strong: HTMLElement;
style: HTMLStyleElement;
sub: HTMLElement;
summary: HTMLElement;
sup: HTMLElement;
table: HTMLTableElement;
tbody: HTMLTableSectionElement;
td: HTMLTableCellElement;
template: HTMLTemplateElement;
textarea: HTMLTextAreaElement;
tfoot: HTMLTableSectionElement;
th: HTMLTableCellElement;
thead: HTMLTableSectionElement;
time: HTMLTimeElement;
title: HTMLTitleElement;
tr: HTMLTableRowElement;
track: HTMLTrackElement;
u: HTMLElement;
ul: HTMLUListElement;
var: HTMLElement;
video: HTMLVideoElement;
wbr: HTMLElement;
} => ({ ...props });
Lastly, you can make a createAllTheElements
reusable function with this type by doing
export const createAllTheElements = (
props: keyof DocumentCreateElementTagNameTandem
):
| HTMLElement
| HTMLObjectElement
| HTMLMapElement
| HTMLLinkElement
| HTMLAnchorElement
| HTMLAreaElement
| HTMLAudioElement
| HTMLBaseElement
| HTMLQuoteElement
| HTMLBodyElement
| HTMLBRElement
| HTMLButtonElement
| HTMLCanvasElement
| HTMLTableCaptionElement
| HTMLTableColElement
| HTMLDataElement
| HTMLDataListElement
| HTMLModElement
| HTMLDetailsElement
| HTMLDialogElement
| HTMLDivElement
| HTMLDListElement
| HTMLEmbedElement
| HTMLFieldSetElement
| HTMLFormElement
| HTMLHeadingElement
| HTMLHeadElement
| HTMLHRElement
| HTMLHtmlElement
| HTMLIFrameElement
| HTMLImageElement
| HTMLInputElement
| HTMLLabelElement
| HTMLLegendElement
| HTMLLIElement
| HTMLMenuElement
| HTMLMetaElement
| HTMLMeterElement
| HTMLOListElement
| HTMLOptGroupElement
| HTMLOptionElement
| HTMLOutputElement
| HTMLParagraphElement
| HTMLPictureElement
| HTMLPreElement
| HTMLProgressElement
| HTMLSlotElement
| HTMLScriptElement
| HTMLSelectElement
| HTMLSourceElement
| HTMLSpanElement
| HTMLStyleElement
| HTMLTableElement
| HTMLTemplateElement
| HTMLTableSectionElement
| HTMLTableCellElement
| HTMLTextAreaElement
| HTMLTimeElement
| HTMLTitleElement
| HTMLTableRowElement
| HTMLTrackElement
| HTMLUListElement
| HTMLVideoElement => document.createElement(props);