215 lines
6.9 KiB
TypeScript
215 lines
6.9 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { getFeaturedProjects } from "@/lib/portfolio-data";
|
|
import NotebookBrowser, { type NotebookBrowserProps } from "./NotebookBrowser";
|
|
import { selectNotebookProjects } from "./notebookProjects";
|
|
|
|
const projects = selectNotebookProjects(getFeaturedProjects(null));
|
|
const frame: NotebookBrowserProps["frame"] = {
|
|
cursorVisible: true,
|
|
cursorX: 0.4,
|
|
cursorY: 0.6,
|
|
cursorClicking: false,
|
|
};
|
|
|
|
function renderBrowser(overrides: Partial<NotebookBrowserProps> = {}) {
|
|
return render(
|
|
<NotebookBrowser
|
|
projects={projects}
|
|
activeIndex={0}
|
|
frame={frame}
|
|
onSelectTab={jest.fn()}
|
|
{...overrides}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe("NotebookBrowser", () => {
|
|
it("renders the active project as an accessible tabpanel", () => {
|
|
renderBrowser({ activeIndex: 1 });
|
|
|
|
expect(
|
|
screen.getByRole("tablist", { name: /projetos em produção/i }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getAllByRole("tab")[1]).toHaveAttribute(
|
|
"aria-selected",
|
|
"true",
|
|
);
|
|
expect(screen.getByRole("tabpanel")).toHaveAccessibleName(
|
|
/SC Plus Aquicultura/i,
|
|
);
|
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
"Projeto ativo: SC Plus Aquicultura",
|
|
);
|
|
});
|
|
it("preloads the next screenshot and keeps a short outgoing crossfade", () => {
|
|
const view = renderBrowser();
|
|
|
|
expect(
|
|
view.container.querySelector(".notebook-screenshot-preload"),
|
|
).toBeInTheDocument();
|
|
|
|
view.rerender(
|
|
<NotebookBrowser
|
|
projects={projects}
|
|
activeIndex={1}
|
|
frame={frame}
|
|
onSelectTab={jest.fn()}
|
|
/>,
|
|
);
|
|
expect(
|
|
view.container.querySelector(".notebook-screenshot-outgoing"),
|
|
).toBeInTheDocument();
|
|
});
|
|
it("moves between tabs with arrow keys and Home/End", async () => {
|
|
const user = userEvent.setup();
|
|
const onSelectTab = jest.fn();
|
|
renderBrowser({ onSelectTab });
|
|
const tabs = screen.getAllByRole("tab");
|
|
|
|
await user.click(tabs[1]);
|
|
await user.keyboard("{ArrowRight}");
|
|
await user.keyboard("{Home}");
|
|
await user.keyboard("{End}");
|
|
|
|
expect(onSelectTab).toHaveBeenNthCalledWith(1, 1);
|
|
expect(onSelectTab).toHaveBeenNthCalledWith(2, 2);
|
|
expect(onSelectTab).toHaveBeenNthCalledWith(3, 0);
|
|
expect(onSelectTab).toHaveBeenNthCalledWith(4, projects.length - 1);
|
|
});
|
|
it("moves DOM focus with the active tab", async () => {
|
|
const user = userEvent.setup();
|
|
const tabs =
|
|
renderBrowser().container.querySelectorAll<HTMLButtonElement>(
|
|
'[role="tab"]',
|
|
);
|
|
|
|
tabs[0].focus();
|
|
await user.keyboard("{ArrowRight}");
|
|
|
|
expect(document.activeElement).toBe(tabs[1]);
|
|
});
|
|
|
|
it("removes the closed browser from the accessibility tree", () => {
|
|
renderBrowser({ interactive: false });
|
|
|
|
const browser = screen.getByTestId("notebook-browser");
|
|
expect(browser).toHaveAttribute("aria-hidden", "true");
|
|
expect(browser).toHaveAttribute("inert");
|
|
});
|
|
|
|
it("bounds arrow navigation at the first and last tab", async () => {
|
|
const user = userEvent.setup();
|
|
const onSelectTab = jest.fn();
|
|
renderBrowser({ onSelectTab });
|
|
const tabs = screen.getAllByRole("tab");
|
|
|
|
await user.click(tabs[0]);
|
|
await user.keyboard("{ArrowLeft}");
|
|
await user.click(tabs.at(-1) as HTMLElement);
|
|
await user.keyboard("{ArrowRight}");
|
|
|
|
expect(onSelectTab).toHaveBeenNthCalledWith(2, 0);
|
|
expect(onSelectTab).toHaveBeenNthCalledWith(4, projects.length - 1);
|
|
});
|
|
|
|
it("keeps an unknown demo link and announces its state", () => {
|
|
const unknownProjects = projects.map((project, index) =>
|
|
index === 0
|
|
? {
|
|
...project,
|
|
demoAvailability: "unknown" as const,
|
|
demoCheckedAt: null,
|
|
}
|
|
: project,
|
|
);
|
|
renderBrowser({ projects: unknownProjects });
|
|
|
|
const link = screen.getByRole("link", {
|
|
name: /abrir projeto.*disponibilidade não verificada/i,
|
|
});
|
|
expect(link).toHaveAttribute("href", projects[0].demoUrl);
|
|
expect(link).toHaveAttribute("target", "_blank");
|
|
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
|
});
|
|
|
|
it("renders an unavailable demo as non-navigable text", () => {
|
|
const unavailableProjects = projects.map((project, index) =>
|
|
index === 0
|
|
? {
|
|
...project,
|
|
demoAvailability: "unavailable" as const,
|
|
demoCheckedAt: "2026-09-04T12:00:00.000Z",
|
|
}
|
|
: project,
|
|
);
|
|
renderBrowser({ projects: unavailableProjects });
|
|
|
|
const unavailable = screen.getByText(
|
|
"Demo indisponível na última verificação",
|
|
);
|
|
expect(unavailable).toHaveAttribute("aria-disabled", "true");
|
|
expect(unavailable.closest("a")).toBeNull();
|
|
expect(
|
|
screen.queryByRole("link", { name: /abrir projeto/i }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders no demo action for a project without a demo", () => {
|
|
const noDemoProjects = [
|
|
{
|
|
...projects[0],
|
|
demoUrl: null,
|
|
demoAvailability: "none",
|
|
demoCheckedAt: null,
|
|
},
|
|
] as unknown as NotebookBrowserProps["projects"];
|
|
renderBrowser({ projects: noDemoProjects });
|
|
|
|
expect(screen.queryByText(/abrir projeto/i)).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText("Demo indisponível na última verificação"),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("keeps the animated cursor decorative and never embeds an iframe", () => {
|
|
const { container } = renderBrowser({
|
|
frame: { ...frame, cursorClicking: true },
|
|
});
|
|
const cursor = container.querySelector(".notebook-cursor");
|
|
|
|
expect(cursor).toHaveAttribute("aria-hidden", "true");
|
|
expect(cursor).toHaveStyle({ left: "40%", top: "60%" });
|
|
expect(cursor?.closest(".notebook-browser")).toBe(
|
|
container.querySelector(".notebook-browser"),
|
|
);
|
|
expect(container.querySelector("iframe")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("marks only the selected tab as pressed by the choreographed cursor", () => {
|
|
const { container } = renderBrowser({
|
|
activeIndex: 1,
|
|
frame: { ...frame, cursorClicking: true },
|
|
});
|
|
const tabs = container.querySelectorAll(".notebook-tab");
|
|
|
|
expect(tabs[1]).toHaveAttribute("data-cursor-clicking", "true");
|
|
expect(tabs[0]).toHaveAttribute("data-cursor-clicking", "false");
|
|
expect(tabs[2]).toHaveAttribute("data-cursor-clicking", "false");
|
|
expect(tabs[3]).toHaveAttribute("data-cursor-clicking", "false");
|
|
});
|
|
|
|
it("uses the selected tab to label the panel and screenshot", () => {
|
|
renderBrowser({ activeIndex: 2 });
|
|
|
|
const tab = screen.getAllByRole("tab")[2];
|
|
const panel = screen.getByRole("tabpanel");
|
|
const image = screen.getByRole("img");
|
|
|
|
expect(tab).toHaveAttribute("aria-controls", panel.id);
|
|
expect(panel).toHaveAttribute("aria-labelledby", tab.id);
|
|
expect(image).toHaveAccessibleName(/SEV Moda Íntima/i);
|
|
expect(image).toHaveAttribute("src", "/assets/prints/print-sev.png");
|
|
});
|
|
});
|