Estado do disco em 14/10/2026, sem filtrar. Inclui as copias .sync-conflict-* do Syncthing de 07/10 e os 15 arquivos que o Syncthing apagou; todo esse conteudo ja existia em outros commits. So havia fora do Git: AGENTS.md, CLAUDE.md e a alteracao em data/project-health.json.
This commit is contained in:
1 parent
b2f3899dc2
commit
dfcadc71fa
77 files changed
+6944
-2419
No files matched your search
@@ -0,0 +1,285 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user