/** * #46 — the project tab bar's "+" add-project button. It must be present, * enabled and visibly a button in EVERY state (notably with zero open projects, * where it is the only way to add a project as a tab), and clicking it opens the * Projects panel. The visibility regression it guards against: the button used * to render with the `ghost` variant (no border/background, muted text) so it * read as faint text rather than a control. */ import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent, within } from "@testing-library/react"; import type { TabItem } from "@/shared"; import { ProjectTabs } from "./ProjectTabs"; const TABS: TabItem[] = [ { id: "a", label: "alpha" }, { id: "b", label: "beta" }, ]; function renderTabs(props: Partial> = {}) { const onOpenProjectsPanel = vi.fn(); render( , ); return { onOpenProjectsPanel }; } describe("ProjectTabs add-project button (#46)", () => { it("renders the + button when projects are open", () => { renderTabs({ items: TABS }); const btn = screen.getByRole("button", { name: "ouvrir le panneau des projets" }); expect(btn).toBeTruthy(); expect(btn.textContent).toContain("+"); expect((btn as HTMLButtonElement).disabled).toBe(false); }); it("still renders the + button with ZERO open projects (only way to add one)", () => { renderTabs({ items: [], activeTabId: null }); // The empty placeholder is shown… expect(screen.getByText("Aucun onglet ouvert.")).toBeTruthy(); // …and the add-project button is still present and enabled. const btn = screen.getByRole("button", { name: "ouvrir le panneau des projets" }); expect(btn).toBeTruthy(); expect((btn as HTMLButtonElement).disabled).toBe(false); }); it("is a visible bordered control, not the near-invisible ghost glyph", () => { // Regression guard for #46: the secondary variant carries a border; the old // ghost variant did not. Asserting the border class keeps the button from // silently reverting to the invisible style. renderTabs({ items: [] }); const btn = screen.getByRole("button", { name: "ouvrir le panneau des projets" }); expect(btn.className).toContain("border"); expect(btn.className).not.toContain("text-muted"); }); it("reflects the panel-open state via aria-pressed", () => { const { onOpenProjectsPanel } = renderTabs({ projectsPanelOpen: true }); const btn = screen.getByRole("button", { name: "ouvrir le panneau des projets" }); expect(btn.getAttribute("aria-pressed")).toBe("true"); expect(onOpenProjectsPanel).not.toHaveBeenCalled(); }); it("opens the Projects panel on click, independent of any active project", () => { const { onOpenProjectsPanel } = renderTabs({ items: [], activeTabId: null }); fireEvent.click(screen.getByRole("button", { name: "ouvrir le panneau des projets" })); expect(onOpenProjectsPanel).toHaveBeenCalledTimes(1); }); }); describe("ProjectTabs project activity dots (#159)", () => { it("renders the neutral activity dot to the left of a project name by default", () => { renderTabs({ items: TABS }); const tab = screen.getByRole("tab", { name: /alpha/ }); expect(within(tab).getByTitle(/alpha.*prêt/i)).toBeTruthy(); }); it("renders idle and busy project activity states", () => { renderTabs({ items: TABS, activityByProjectId: { a: "idle", b: "busy" }, }); expect(screen.getByTitle(/alpha.*prêt/i)).toBeTruthy(); expect(screen.getByTitle(/beta.*échange en cours/i)).toBeTruthy(); }); });