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 >) => {children}, })); const base = buildShowcaseItems(getFeaturedProjects(null)).find( ({ project }) => project.demoUrl, ) as ShowcaseItem; function item(overrides: Partial): 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("[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( , ); 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(); 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(); expect(screen.queryByRole("link", { name: /Abrir site/ })).toBeNull(); }); it("path: the expand button runs onExpand", () => { const onExpand = jest.fn(); render(); 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(); 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(); 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(); 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(); 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(); 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( , ); act(() => link(/Abrir site/).focus()); expect(tabindexes()).toContain("site:0"); rerender( , ); expect(tabindexes()).toEqual(["case:0", "expand:-1"]); }); });