import { act, fireEvent, render, screen } from "@testing-library/react"; import type { KeyboardEvent } from "react"; import { moveRovingFocus, useRovingTabindex } from "./useRovingTabindex"; /** * Testes de caminhos-base de moveRovingFocus e useRovingTabindex (complemento de useRovingTabindex.test.tsx): * cada tecla de gesto, cada modificador sozinho, grupo de um item só, o item atual escondido por CSS, um alvo que * não é elemento, o foco em algo do grupo que não é item e a parada que deixa de valer. */ const KEYS = ["p:0", "p:1", "p:2"]; function Group({ count = 3, validBelow = 99, }: { count?: number; /** Chaves `p:` com n a partir daqui deixam de valer (o item some do grupo). */ validBelow?: number; }) { const { groupProps, itemProps } = useRovingTabindex("p:0", (key) => { const [, index] = key.split(":"); return Number(index) < validBelow; }); return ( ); } const item = (index: number) => screen.getByRole("button", { name: `item ${index}` }); describe("moveRovingFocus: um gesto por tecla", () => { it.each([ ["ArrowDown", 2], ["ArrowRight", 2], ["ArrowUp", 0], ["ArrowLeft", 0], ["Home", 0], ["End", 2], ])( "path: %s from the middle item -> focus on item %i, the default is cancelled", (key, expected) => { render(); item(1).focus(); expect(fireEvent.keyDown(item(1), { key })).toBe(false); expect(item(expected)).toHaveFocus(); }, ); it.each(["altKey", "ctrlKey", "metaKey", "shiftKey"] as const)( "path: ArrowDown with %s alone -> left to the browser (not cancelled, focus stays)", (modifier) => { render(); item(0).focus(); expect( fireEvent.keyDown(item(0), { key: "ArrowDown", [modifier]: true }), ).toBe(true); expect(item(0)).toHaveFocus(); }, ); it("path: a group with one item only -> every gesture stays on it (and is still cancelled)", () => { render(); item(0).focus(); for (const key of ["ArrowDown", "ArrowUp", "Home", "End"]) { expect(fireEvent.keyDown(item(0), { key })).toBe(false); expect(item(0)).toHaveFocus(); } }); }); describe("moveRovingFocus: quando o gesto não é do grupo", () => { it("path: the current item is itself hidden by CSS -> it is not among the shown ones, so the key is left alone", () => { const original = HTMLElement.prototype.checkVisibility; HTMLElement.prototype.checkVisibility = function check(this: HTMLElement) { return this.textContent !== "item 1"; }; try { render(); item(1).focus(); expect(fireEvent.keyDown(item(1), { key: "ArrowDown" })).toBe(true); expect(item(1)).toHaveFocus(); } finally { HTMLElement.prototype.checkVisibility = original; } }); it("path: the browser has no checkVisibility -> every item counts as shown", () => { const original = HTMLElement.prototype.checkVisibility; Reflect.deleteProperty(HTMLElement.prototype, "checkVisibility"); try { render(); item(0).focus(); expect(fireEvent.keyDown(item(0), { key: "End" })).toBe(false); expect(item(2)).toHaveFocus(); } finally { HTMLElement.prototype.checkVisibility = original; } }); it("path: the key target is not an element (the document) -> nothing happens", () => { const preventDefault = jest.fn(); const group = document.createElement("div"); moveRovingFocus({ key: "ArrowDown", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, target: document, currentTarget: group, preventDefault, } as unknown as KeyboardEvent); expect(preventDefault).not.toHaveBeenCalled(); }); it("path: a key that is not a gesture (a letter) -> left alone", () => { render(); item(0).focus(); expect(fireEvent.keyDown(item(0), { key: "a" })).toBe(true); }); }); describe("useRovingTabindex: a parada do Tab", () => { it("path: focus on something of the group that is not an item -> the tab stop does not move", () => { render(); act(() => item(2).focus()); expect(item(2)).toHaveAttribute("tabindex", "0"); act(() => screen.getByRole("link", { name: "link comum" }).focus()); expect(item(2)).toHaveAttribute("tabindex", "0"); expect(item(0)).toHaveAttribute("tabindex", "-1"); }); it("path: the remembered stop stops being valid (that item left the group) -> back to the fallback", () => { const { rerender } = render(); act(() => item(2).focus()); expect(item(2)).toHaveAttribute("tabindex", "0"); rerender(); expect(item(0)).toHaveAttribute("tabindex", "0"); expect(item(1)).toHaveAttribute("tabindex", "-1"); }); });