fix(navegacao): foco em âncora de outra página, vitrine acessível e 3D mais leve
Foco cai no destino ao chegar de outra página; âncoras ignoram ?tipo=; movimento reduzido salta em todos os cliques; cronômetro da janela ampliada; degradê no topo de Projetos e legendas do mergulho com intervalos testados; vitrine acima da barra de cookies e com 2 paradas por projeto; reel-list.css no global; 3D montado em tarefas curtas, limitado a ~30 qps e estático com WebGL por software; pôsteres da vitrine de 330 para 158 KB; SC Plus regravado; scripts sem funções acima de complexidade 10.
This commit is contained in:
1 parent
9e2d65a926
commit
204ebd5fa5
49 files changed
+2197
-606
No files matched your search
@@ -0,0 +1,168 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { getFeaturedProjects } from "@/lib/portfolio-data";
|
||||
import { canOpenSite, ProjectActions } from "./ProjectActions";
|
||||
import { buildShowcaseItems, type ShowcaseItem } from "./ShowcaseProvider";
|
||||
|
||||
/**
|
||||
* Testes de caminhos-base de ProjectActions (CC 7) e canOpenSite (CC 2).
|
||||
* Sem `grouped` (a lista): "Ver case", "Ampliar vídeo" (só com onExpand) e "Abrir site" (só com site no ar) são três
|
||||
* paradas do Tab cada uma. Com `grouped` (a vitrine fixa): uma barra de ferramentas e UMA parada, o roving tabindex:
|
||||
* a parada é "Ver case" até o foco passar a outra ação que ainda exista; as setas e Home/End movem o foco.
|
||||
* Combinações: grouped x (com | sem onExpand) x (com | sem site).
|
||||
*/
|
||||
|
||||
jest.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
children,
|
||||
prefetch: _prefetch,
|
||||
...props
|
||||
}: { children?: React.ReactNode; prefetch?: unknown } & Record<
|
||||
string,
|
||||
unknown
|
||||
>) => <a {...props}>{children}</a>,
|
||||
}));
|
||||
|
||||
const base = buildShowcaseItems(getFeaturedProjects(null)).find(
|
||||
({ project }) => project.demoUrl,
|
||||
) as ShowcaseItem;
|
||||
|
||||
function item(overrides: Partial<ShowcaseItem["project"]>): ShowcaseItem {
|
||||
return { ...base, project: { ...base.project, ...overrides } };
|
||||
}
|
||||
|
||||
const withSite = item({
|
||||
demoUrl: "https://exemplo.com.br",
|
||||
demoAvailability: "available",
|
||||
});
|
||||
const withoutSite = item({ demoUrl: null, demoAvailability: "unknown" });
|
||||
const siteDown = item({
|
||||
demoUrl: "https://exemplo.com.br",
|
||||
demoAvailability: "unavailable",
|
||||
});
|
||||
|
||||
const link = (name: RegExp) => screen.getByRole("link", { name });
|
||||
const tabindexes = () =>
|
||||
Array.from(document.querySelectorAll<HTMLElement>("[data-roving]")).map(
|
||||
(element) => `${element.getAttribute("data-roving")}:${element.tabIndex}`,
|
||||
);
|
||||
const press = (key: string) =>
|
||||
fireEvent.keyDown(document.activeElement as Element, { key });
|
||||
|
||||
describe("canOpenSite", () => {
|
||||
it.each([
|
||||
["site no ar", withSite, true],
|
||||
["sem endereço", withoutSite, false],
|
||||
["endereço dado como fora do ar", siteDown, false],
|
||||
])("path: %s -> %s", (_name, project, expected) => {
|
||||
expect(canOpenSite(project.project)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectActions sem grouped (a lista)", () => {
|
||||
it("path: expand and a live site -> three actions, no toolbar, no roving attributes (each is its own Tab stop)", () => {
|
||||
const { container } = render(
|
||||
<ProjectActions item={withSite} onExpand={jest.fn()} />,
|
||||
);
|
||||
|
||||
expect(link(/Ver case/)).toBeVisible();
|
||||
expect(screen.getByRole("button", { name: /Ampliar/ })).toBeVisible();
|
||||
expect(link(/Abrir site/)).toHaveAttribute("target", "_blank");
|
||||
expect(screen.queryByRole("toolbar")).toBeNull();
|
||||
expect(container.querySelector("[data-roving]")).toBeNull();
|
||||
expect(container.querySelector("[tabindex]")).toBeNull();
|
||||
});
|
||||
|
||||
it("path: no onExpand (before the page mounts) and no site -> only 'Ver case'", () => {
|
||||
render(<ProjectActions item={withoutSite} />);
|
||||
|
||||
expect(link(/Ver case/)).toBeVisible();
|
||||
expect(screen.queryByRole("button")).toBeNull();
|
||||
expect(screen.queryByRole("link", { name: /Abrir site/ })).toBeNull();
|
||||
});
|
||||
|
||||
it("path: a site declared down -> no 'Abrir site' even with an address", () => {
|
||||
render(<ProjectActions item={siteDown} onExpand={jest.fn()} />);
|
||||
|
||||
expect(screen.queryByRole("link", { name: /Abrir site/ })).toBeNull();
|
||||
});
|
||||
|
||||
it("path: the expand button runs onExpand", () => {
|
||||
const onExpand = jest.fn();
|
||||
render(<ProjectActions item={withSite} onExpand={onExpand} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Ampliar/ }));
|
||||
|
||||
expect(onExpand).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectActions com grouped (a vitrine fixa)", () => {
|
||||
it("path: all three actions -> one toolbar named after the project, one Tab stop ('Ver case'), the others at -1", () => {
|
||||
render(<ProjectActions item={withSite} onExpand={jest.fn()} grouped />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("toolbar", {
|
||||
name: `Ações de ${withSite.project.title}`,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(tabindexes()).toEqual(["case:0", "expand:-1", "site:-1"]);
|
||||
});
|
||||
|
||||
it("path: the arrows walk the three actions in order and stop at the ends; Home and End jump; Tab is left alone", () => {
|
||||
render(<ProjectActions item={withSite} onExpand={jest.fn()} grouped />);
|
||||
const expand = screen.getByRole("button", { name: /Ampliar/ });
|
||||
|
||||
act(() => link(/Ver case/).focus());
|
||||
press("ArrowRight");
|
||||
expect(expand).toHaveFocus();
|
||||
press("ArrowRight");
|
||||
expect(link(/Abrir site/)).toHaveFocus();
|
||||
press("ArrowRight");
|
||||
expect(link(/Abrir site/)).toHaveFocus();
|
||||
press("Home");
|
||||
expect(link(/Ver case/)).toHaveFocus();
|
||||
press("End");
|
||||
expect(link(/Abrir site/)).toHaveFocus();
|
||||
// o Tab sai do grupo em vez de andar dentro dele: não é cancelado
|
||||
expect(press("Tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("path: the Tab stop follows the last focused action, so Shift+Tab and Tab come back to it", () => {
|
||||
render(<ProjectActions item={withSite} onExpand={jest.fn()} grouped />);
|
||||
|
||||
act(() => link(/Abrir site/).focus());
|
||||
|
||||
expect(tabindexes()).toEqual(["case:-1", "expand:-1", "site:0"]);
|
||||
});
|
||||
|
||||
it("path: no site -> the toolbar has two actions and the arrows never reach a missing 'Abrir site'", () => {
|
||||
render(<ProjectActions item={withoutSite} onExpand={jest.fn()} grouped />);
|
||||
|
||||
expect(tabindexes()).toEqual(["case:0", "expand:-1"]);
|
||||
act(() => screen.getByRole("button", { name: /Ampliar/ }).focus());
|
||||
press("ArrowRight");
|
||||
expect(screen.getByRole("button", { name: /Ampliar/ })).toHaveFocus();
|
||||
});
|
||||
|
||||
it("path: no onExpand and no site -> a toolbar of one action, which is the Tab stop", () => {
|
||||
render(<ProjectActions item={withoutSite} grouped />);
|
||||
|
||||
expect(tabindexes()).toEqual(["case:0"]);
|
||||
expect(screen.getByRole("toolbar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("path: the stop was on 'Abrir site' and the next render has no site -> back to 'Ver case', never a missing stop", () => {
|
||||
const { rerender } = render(
|
||||
<ProjectActions item={withSite} onExpand={jest.fn()} grouped />,
|
||||
);
|
||||
act(() => link(/Abrir site/).focus());
|
||||
expect(tabindexes()).toContain("site:0");
|
||||
|
||||
rerender(
|
||||
<ProjectActions item={withoutSite} onExpand={jest.fn()} grouped />,
|
||||
);
|
||||
|
||||
expect(tabindexes()).toEqual(["case:0", "expand:-1"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user