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 = {}) { return render( , ); } 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( , ); let finishDecode!: () => void; imageFor(view.container, 1).decode = jest .fn() .mockResolvedValueOnce(undefined) .mockImplementation( () => new Promise((resolve) => { finishDecode = resolve; }), ); await act(async () => { fireEvent.load(imageFor(view.container, 1)); }); expect(screen.getByRole("img")).toHaveAccessibleName(/NutriScan/i); view.rerender( , ); 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( , ); 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( , ); await act(async () => { fireEvent.load(imageFor(view.container, 1)); }); view.rerender( , ); 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( '[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, ); }); });