Files
portifolio-evandrodevbr/app/components/showcase/useRovingTabindex.paths.test.tsx
Evandro Fonseca Junior 6ac87824f3 perf(movimento): sem tempo morto, vídeos regravados e 3D sem React Three Fiber
Legenda do mergulho até Projetos cobrir a cena; transição de página sem blur e com fundo escuro; foco no conteúdo ao trocar de página; listas com marcador e dois pesos de título; vídeos da SEV e da SC Plus regravados sem quadros brancos; scrub a 12-15 fps (-30%); vitrine com uma parada de Tab e setas, contador no celular e CSS sob demanda; âncoras num módulo só (fragment-nav); FramePulse com setInterval; CSS morto removido.
2026-10-14 17:01:37 -03:00

167 lines
5.0 KiB
TypeScript

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:<n>` 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 (
<nav aria-label="grupo" {...groupProps}>
{KEYS.slice(0, count).map((key) => (
<button key={key} type="button" {...itemProps(key)}>
{`item ${key.slice(2)}`}
</button>
))}
<a href="#fora">link comum</a>
</nav>
);
}
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(<Group />);
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(<Group />);
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(<Group count={1} />);
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(<Group />);
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(<Group />);
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<HTMLElement>);
expect(preventDefault).not.toHaveBeenCalled();
});
it("path: a key that is not a gesture (a letter) -> left alone", () => {
render(<Group />);
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(<Group />);
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(<Group />);
act(() => item(2).focus());
expect(item(2)).toHaveAttribute("tabindex", "0");
rerender(<Group count={2} validBelow={2} />);
expect(item(0)).toHaveAttribute("tabindex", "0");
expect(item(1)).toHaveAttribute("tabindex", "-1");
});
});