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.
167 lines
5.0 KiB
TypeScript
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");
|
|
});
|
|
});
|