coder/site/src/hooks/useClickableTableRow.test.tsx

155 lines
4.8 KiB
TypeScript

import {
type ElementType,
type FC,
type MouseEventHandler,
type PropsWithChildren,
} from "react";
import { type ClickableAriaRole, useClickable } from "./useClickable";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
/**
* Since the point of the hook is to take a traditionally non-interactive HTML
* element and make it interactive, it made the most sense to test against an
* element directly.
*/
type NonNativeButtonProps<TElement extends HTMLElement = HTMLElement> =
Readonly<
PropsWithChildren<{
as?: Exclude<ElementType, "button">;
role?: ClickableAriaRole;
onInteraction: MouseEventHandler<TElement>;
}>
>;
const NonNativeButton: FC<NonNativeButtonProps<HTMLElement>> = ({
as,
onInteraction,
children,
role = "button",
}) => {
const clickableProps = useClickable(onInteraction, role);
const Component = as ?? "div";
return <Component {...clickableProps}>{children}</Component>;
};
describe(useClickable.name, () => {
it("Always defaults to role 'button'", () => {
render(<NonNativeButton onInteraction={jest.fn()} />);
expect(() => screen.getByRole("button")).not.toThrow();
});
it("Overrides the native role of any element that receives the hook result (be very careful with this behavior)", () => {
const anchorText = "I'm a button that's secretly a link!";
render(
<NonNativeButton as="a" role="button" onInteraction={jest.fn()}>
{anchorText}
</NonNativeButton>,
);
const linkButton = screen.getByRole("button", {
name: anchorText,
});
expect(linkButton).toBeInstanceOf(HTMLAnchorElement);
});
it("Always returns out the same role override received via arguments", () => {
const placeholderCallback = jest.fn();
const roles = [
"button",
"switch",
] as const satisfies readonly ClickableAriaRole[];
for (const role of roles) {
const { unmount } = render(
<NonNativeButton role={role} onInteraction={placeholderCallback} />,
);
expect(() => screen.getByRole(role)).not.toThrow();
unmount();
}
});
it("Allows an element to receive keyboard focus", async () => {
const user = userEvent.setup();
const mockCallback = jest.fn();
render(<NonNativeButton role="button" onInteraction={mockCallback} />, {
wrapper: ({ children }) => (
<>
<button>Native button</button>
{children}
</>
),
});
await user.keyboard("[Tab][Tab]");
await user.keyboard(" ");
expect(mockCallback).toBeCalledTimes(1);
});
it("Allows an element to respond to clicks and Space/Enter, following all rules for native Button element interactions", async () => {
const mockCallback = jest.fn();
const user = userEvent.setup();
render(<NonNativeButton role="button" onInteraction={mockCallback} />);
await user.click(document.body);
await user.keyboard(" ");
await user.keyboard("[Enter]");
expect(mockCallback).not.toBeCalled();
const button = screen.getByRole("button");
await user.click(button);
await user.keyboard(" ");
await user.keyboard("[Enter]");
expect(mockCallback).toBeCalledTimes(3);
});
it("Will keep firing events if the Enter key is held down", async () => {
const mockCallback = jest.fn();
const user = userEvent.setup();
render(<NonNativeButton role="button" onInteraction={mockCallback} />);
// Focus over to element, hold down Enter for specified keydown period
// (count determined by browser/library), and then release the Enter key
const keydownCount = 5;
await user.keyboard(`[Tab]{Enter>${keydownCount}}{/Enter}`);
expect(mockCallback).toBeCalledTimes(keydownCount);
});
it("Will NOT keep firing events if the Space key is held down", async () => {
const mockCallback = jest.fn();
const user = userEvent.setup();
render(<NonNativeButton role="button" onInteraction={mockCallback} />);
// Focus over to element, and then hold down Space for 100 keydown cycles
await user.keyboard("[Tab]{ >100}");
expect(mockCallback).not.toBeCalled();
// Then explicitly release the space bar
await user.keyboard(`{/ }`);
expect(mockCallback).toBeCalledTimes(1);
});
test("If focus is lost while Space is held down, then releasing the key will do nothing", async () => {
const mockCallback = jest.fn();
const user = userEvent.setup();
render(<NonNativeButton role="button" onInteraction={mockCallback} />, {
wrapper: ({ children }) => (
<>
{children}
<button>Native button</button>
</>
),
});
// Focus over to element, hold down Space for an indefinite amount of time,
// move focus away from element, and then release Space
await user.keyboard("[Tab]{ >}[Tab]{/ }");
expect(mockCallback).not.toBeCalled();
});
});