/* * Copyright 2021 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useMemo } from "react"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest"; import { InputGroup } from "../components/forms/inputGroup"; import { HotkeysProvider } from "../context"; import { useHotkeys } from "./"; interface TestComponentProps extends TestComponentContainerProps { onKeyA: () => void; onKeyB: () => void; } interface TestComponentContainerProps { bindExtraKeys?: boolean; isInputReadOnly?: boolean; showDialogKeyCombo?: string | false; } const TestComponent: React.FC = ({ bindExtraKeys, isInputReadOnly, onKeyA, onKeyB, showDialogKeyCombo, }) => { const hotkeys = useMemo(() => { const keys = [ { combo: "A", label: "A", onKeyDown: onKeyA, }, { combo: "B", global: true, label: "B", onKeyDown: onKeyB, }, ]; if (bindExtraKeys) { keys.push( { combo: "shift+A", label: "shift+A", onKeyDown: onKeyA, }, { combo: "shift+B", global: true, label: "shift+B", onKeyDown: onKeyB, }, ); } return keys; }, [bindExtraKeys, onKeyA, onKeyB]); const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys, { showDialogKeyCombo }); return (
); }; describe("useHotkeys", () => { const onKeyASpy = vi.fn(); const onKeyBSpy = vi.fn(); const TestComponentContainer = (props: TestComponentContainerProps) => { return ( <>
); }; afterEach(() => { onKeyASpy.mockClear(); onKeyBSpy.mockClear(); }); it("binds local hotkey", async () => { const user = userEvent.setup(); render(); const target = screen.getByTestId("target-inside-component"); target.focus(); await user.keyboard("a"); expect(onKeyASpy).toHaveBeenCalledOnce(); }); it("binds global hotkey", async () => { const user = userEvent.setup(); render(); const target = screen.getByTestId("target-outside-component"); target.focus(); await user.keyboard("b"); expect(onKeyBSpy).toHaveBeenCalledOnce(); }); it("binds new local hotkeys when hook arg is updated", async () => { const user = userEvent.setup(); const { rerender } = render(); rerender(); const target = screen.getByTestId("target-inside-component"); target.focus(); // bindExtraKeys adds "shift+A" combo, so we need Shift held during keypress await user.keyboard("{Shift>}a{/Shift}"); expect(onKeyASpy).toHaveBeenCalledOnce(); }); it("binds new global hotkeys when hook arg is updated", async () => { const user = userEvent.setup(); const { rerender } = render(); rerender(); const target = screen.getByTestId("target-outside-component"); target.focus(); // bindExtraKeys adds "shift+B" combo, so we need Shift held during keypress await user.keyboard("{Shift>}b{/Shift}"); expect(onKeyBSpy).toHaveBeenCalledOnce(); }); it("removes local hotkeys when hook arg is updated", async () => { const user = userEvent.setup(); const { rerender } = render(); rerender(); const target = screen.getByTestId("target-inside-component"); target.focus(); // "shift+A" combo should no longer be bound after removing extra keys await user.keyboard("{Shift>}a{/Shift}"); expect(onKeyASpy).not.toHaveBeenCalled(); }); it("removes global hotkeys when hook arg is updated", async () => { const user = userEvent.setup(); const { rerender } = render(); rerender(); const target = screen.getByTestId("target-outside-component"); target.focus(); // "shift+B" combo should no longer be bound after removing extra keys await user.keyboard("{Shift>}b{/Shift}"); expect(onKeyBSpy).not.toHaveBeenCalled(); }); it("does not trigger hotkeys inside text inputs", async () => { const user = userEvent.setup(); render(); const target = screen.getByTestId("input-target"); target.focus(); await user.keyboard("a"); expect(onKeyASpy).not.toHaveBeenCalled(); }); it("does trigger hotkeys inside readonly text inputs", async () => { const user = userEvent.setup(); render(); const target = screen.getByTestId("input-target"); target.focus(); await user.keyboard("a"); expect(onKeyASpy).toHaveBeenCalledOnce(); }); describe("working with HotkeysProvider", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn()); beforeEach(() => warnSpy.mockClear()); afterAll(() => warnSpy.mockRestore()); it("logs a warning when used outside of HotkeysProvider context", () => { render(); expect(warnSpy).toHaveBeenCalledOnce(); }); it("does NOT log a warning when used inside a HotkeysProvider context", () => { render( , ); expect(warnSpy).not.toHaveBeenCalled(); }); }); describe("showDialogKeyCombo", () => { it("opens the help dialog when the default combo (?) is pressed", async () => { const user = userEvent.setup(); render( , ); expect(screen.queryByRole("dialog")).toBeNull(); screen.getByTestId("target-outside-component").focus(); await user.keyboard("{Shift>}/{/Shift}"); expect(await screen.findByRole("dialog")).toBeInTheDocument(); }); it("does not open the help dialog when showDialogKeyCombo is false", async () => { const user = userEvent.setup(); render( , ); screen.getByTestId("target-outside-component").focus(); await user.keyboard("{Shift>}/{/Shift}"); expect(screen.queryByRole("dialog")).toBeNull(); }); }); });