CI / verify (push) Failing after 5m9s
Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
286 lines
8.7 KiB
TypeScript
286 lines
8.7 KiB
TypeScript
import { act, fireEvent, 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}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
function imageFor(container: HTMLElement, index: number) {
|
|
const source = new URL(projects[index].screenshotSrc, document.baseURI).href;
|
|
const image = Array.from(container.querySelectorAll("img")).find(
|
|
(candidate) => candidate.src === source,
|
|
);
|
|
if (!image) throw new Error(`Missing screenshot for ${projects[index].id}`);
|
|
return image;
|
|
}
|
|
|
|
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("keeps decoded content visible and ignores a superseded decode", async () => {
|
|
const view = renderBrowser();
|
|
await act(async () => {
|
|
fireEvent.load(imageFor(view.container, 0));
|
|
});
|
|
expect(screen.getByRole("img")).toHaveAccessibleName(/NutriScan/i);
|
|
|
|
view.rerender(
|
|
<NotebookBrowser
|
|
projects={projects}
|
|
activeIndex={1}
|
|
frame={frame}
|
|
onSelectTab={jest.fn()}
|
|
/>,
|
|
);
|
|
let finishDecode!: () => void;
|
|
imageFor(view.container, 1).decode = jest
|
|
.fn()
|
|
.mockResolvedValueOnce(undefined)
|
|
.mockImplementation(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
finishDecode = resolve;
|
|
}),
|
|
);
|
|
await act(async () => {
|
|
fireEvent.load(imageFor(view.container, 1));
|
|
});
|
|
expect(screen.getByRole("img")).toHaveAccessibleName(/NutriScan/i);
|
|
|
|
view.rerender(
|
|
<NotebookBrowser
|
|
projects={projects}
|
|
activeIndex={2}
|
|
frame={frame}
|
|
onSelectTab={jest.fn()}
|
|
/>,
|
|
);
|
|
await act(async () => {
|
|
fireEvent.load(imageFor(view.container, 2));
|
|
});
|
|
expect(screen.getByRole("img")).toHaveAccessibleName(/SEV Moda Íntima/i);
|
|
await act(async () => {
|
|
finishDecode();
|
|
});
|
|
expect(screen.getByRole("img")).toHaveAccessibleName(/SEV Moda Íntima/i);
|
|
});
|
|
|
|
it("keeps the last capture with a truthful notice when the requested image fails", async () => {
|
|
const view = renderBrowser();
|
|
await act(async () => {
|
|
fireEvent.load(imageFor(view.container, 0));
|
|
});
|
|
view.rerender(
|
|
<NotebookBrowser
|
|
projects={projects}
|
|
activeIndex={1}
|
|
frame={frame}
|
|
onSelectTab={jest.fn()}
|
|
/>,
|
|
);
|
|
fireEvent.error(imageFor(view.container, 1));
|
|
expect(screen.getByRole("img")).toHaveAccessibleName(/NutriScan/i);
|
|
expect(
|
|
screen.getByText(/não foi possível carregar.*SC Plus/i),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByRole("tabpanel")).toHaveAccessibleName(/SC Plus/i);
|
|
});
|
|
|
|
it("can reverse to an already-loaded outgoing image without another load event", async () => {
|
|
const view = renderBrowser();
|
|
const first = imageFor(view.container, 0);
|
|
Object.defineProperties(first, {
|
|
complete: { value: true },
|
|
naturalWidth: { value: 1280 },
|
|
});
|
|
await act(async () => {
|
|
fireEvent.load(first);
|
|
});
|
|
view.rerender(
|
|
<NotebookBrowser
|
|
projects={projects}
|
|
activeIndex={1}
|
|
frame={frame}
|
|
onSelectTab={jest.fn()}
|
|
/>,
|
|
);
|
|
await act(async () => {
|
|
fireEvent.load(imageFor(view.container, 1));
|
|
});
|
|
view.rerender(
|
|
<NotebookBrowser
|
|
projects={projects}
|
|
activeIndex={0}
|
|
frame={frame}
|
|
onSelectTab={jest.fn()}
|
|
/>,
|
|
);
|
|
expect(
|
|
await screen.findByRole("img", { name: /NutriScan/i }),
|
|
).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("uses the selected tab to label the panel and decoded screenshot", async () => {
|
|
const view = renderBrowser({ activeIndex: 2 });
|
|
await act(async () => {
|
|
fireEvent.load(imageFor(view.container, 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).toHaveProperty(
|
|
"src",
|
|
new URL(projects[2].screenshotSrc, document.baseURI).href,
|
|
);
|
|
});
|
|
});
|