/** * L8 — git feature wired to the stateful `MockGitGateway` via the real * `DIProvider`. * * Covers: * - status shows staged/unstaged groupings * - indexer moves a file from Unstaged to Staged * - commit with empty message is blocked (button disabled) * - commit with message but nothing staged is blocked (button disabled) * - commit moves staged files out and adds an entry to the log * - checkout changes the current branch * - MockGitGateway unit behaviour */ import { describe, it, expect } from "vitest"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; import { MockGitGateway } from "@/adapters/mock"; import type { Gateways } from "@/ports"; import { DIProvider } from "@/app/di"; import { GitPanel } from "./GitPanel"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const PROJECT_ID = "proj-git-test-001"; function renderPanel(git: MockGitGateway = new MockGitGateway()) { const gateways = { git } as unknown as Gateways; return { git, ...render( , ), }; } /** Waits until the "Changes" section is rendered (panel stabilised). */ async function waitForPanel() { await waitFor(() => { expect(screen.getByRole("region", { name: "Modifications" })).toBeTruthy(); }); } // --------------------------------------------------------------------------- // Integration tests (GitPanel + MockGitGateway via DIProvider) // --------------------------------------------------------------------------- describe("GitPanel (with MockGitGateway)", () => { it("shows demo files split between Staged and Unstaged sections", async () => { const git = new MockGitGateway(); // Seed a staged file. await git.init(PROJECT_ID); await git.stage(PROJECT_ID, "src/main.rs"); renderPanel(git); await waitForPanel(); await screen.findByText("Indexées"); await screen.findByText("Non indexées"); // src/main.rs is staged → Unindexer button exists expect( screen.getByRole("button", { name: "désindexer src/main.rs" }), ).toBeTruthy(); // README.md is unstaged → Stage button exists expect( screen.getByRole("button", { name: "indexer README.md" }), ).toBeTruthy(); }); it("clicking Stage moves a file to the Staged group", async () => { renderPanel(); await waitForPanel(); // Stage README.md const stageBtn = await screen.findByRole("button", { name: "indexer README.md", }); fireEvent.click(stageBtn); // After staging, an Unindexer button should appear for README.md await waitFor(() => { expect( screen.getByRole("button", { name: "désindexer README.md" }), ).toBeTruthy(); }); }); it("clicking Unindexer moves a file back to Unstaged", async () => { const git = new MockGitGateway(); await git.init(PROJECT_ID); await git.stage(PROJECT_ID, "src/main.rs"); renderPanel(git); await waitForPanel(); const unstageBtn = await screen.findByRole("button", { name: "désindexer src/main.rs", }); fireEvent.click(unstageBtn); await waitFor(() => { expect( screen.getByRole("button", { name: "indexer src/main.rs" }), ).toBeTruthy(); }); }); it("Commit button is disabled when message is empty", async () => { renderPanel(); await waitForPanel(); const btn = screen.getByRole("button", { name: /commit/i }); expect((btn as HTMLButtonElement).disabled).toBe(true); }); it("Commit button is disabled when message is set but nothing is staged", async () => { renderPanel(); await waitForPanel(); fireEvent.change(screen.getByLabelText("message de commit"), { target: { value: "my message" }, }); const btn = screen.getByRole("button", { name: /commit/i }); expect((btn as HTMLButtonElement).disabled).toBe(true); }); it("successful commit removes staged files and adds to the log", async () => { const git = new MockGitGateway(); await git.init(PROJECT_ID); await git.stage(PROJECT_ID, "src/main.rs"); await git.stage(PROJECT_ID, "README.md"); renderPanel(git); await waitForPanel(); fireEvent.change(screen.getByLabelText("message de commit"), { target: { value: "initial commit" }, }); const btn = screen.getByRole("button", { name: /commit/i }); expect((btn as HTMLButtonElement).disabled).toBe(false); fireEvent.click(btn); // After commit, no files should remain (both were staged). await waitFor(() => { expect(screen.getByText("Aucune modification.")).toBeTruthy(); }); // The commit should appear in the log. await waitFor(() => { expect(screen.getByText("initial commit")).toBeTruthy(); }); }); it("checkout changes the current branch label", async () => { renderPanel(); await waitForPanel(); // Ensure the Branches section is rendered with "main" as current. const branchesSection = await screen.findByRole("region", { name: "Branches", }); expect(branchesSection).toBeTruthy(); // Create & checkout a new branch via the form. const branchInput = screen.getByLabelText("nom de la nouvelle branche"); fireEvent.change(branchInput, { target: { value: "feature/new-thing" } }); fireEvent.click(screen.getByRole("button", { name: /créer.*basculer/i })); // After checkout, the new branch should appear (may show in current + list). await waitFor(() => { const matches = screen.getAllByText("feature/new-thing"); expect(matches.length).toBeGreaterThan(0); }); // A checkout button for "main" should now appear (since it's no longer current). await waitFor(() => { expect( screen.getByRole("button", { name: "basculer sur main" }), ).toBeTruthy(); }); }); it("shows 'No commits yet.' in the Log section initially", async () => { renderPanel(); await waitForPanel(); expect(screen.getByText("Aucun commit.")).toBeTruthy(); }); }); // --------------------------------------------------------------------------- // MockGitGateway unit tests // --------------------------------------------------------------------------- describe("MockGitGateway (unit)", () => { it("status returns seeded files for a new project", async () => { const gw = new MockGitGateway(); const files = await gw.status("proj"); expect(files.length).toBeGreaterThan(0); expect(files.every((f) => !f.staged)).toBe(true); }); it("init creates state if absent", async () => { const gw = new MockGitGateway(); await gw.init("new-proj"); const files = await gw.status("new-proj"); expect(Array.isArray(files)).toBe(true); }); it("indexer toggles a file to staged=true", async () => { const gw = new MockGitGateway(); const before = await gw.status("p"); const target = before[0].path; await gw.stage("p", target); const after = await gw.status("p"); const file = after.find((f) => f.path === target)!; expect(file.staged).toBe(true); }); it("désindexer toggles a file back to staged=false", async () => { const gw = new MockGitGateway(); const files = await gw.status("p"); const target = files[0].path; await gw.stage("p", target); await gw.unstage("p", target); const after = await gw.status("p"); expect(after.find((f) => f.path === target)!.staged).toBe(false); }); it("commit removes staged files from status and adds to log", async () => { const gw = new MockGitGateway(); const files = await gw.status("p"); const target = files[0].path; await gw.stage("p", target); const result = await gw.commit("p", "test commit\n\nmore text"); expect(result.summary).toBe("test commit"); expect(result.hash).toMatch(/^mock-/); const after = await gw.status("p"); expect(after.find((f) => f.path === target)).toBeUndefined(); const log = await gw.log("p", 10); expect(log[0].summary).toBe("test commit"); }); it("branches returns initial ['main'] with current='main'", async () => { const gw = new MockGitGateway(); const b = await gw.branches("p"); expect(b.branches).toEqual(["main"]); expect(b.current).toBe("main"); }); it("checkout creates a new branch and sets it as current", async () => { const gw = new MockGitGateway(); await gw.checkout("p", "develop"); const b = await gw.branches("p"); expect(b.branches).toContain("develop"); expect(b.current).toBe("develop"); }); it("log(limit) returns at most limit entries", async () => { const gw = new MockGitGateway(); const files = await gw.status("p"); // Stage and commit each file individually. for (let i = 0; i < files.length; i++) { await gw.stage("p", files[i].path); await gw.commit("p", `commit ${i}`); } const log = await gw.log("p", 1); expect(log).toHaveLength(1); }); it("projects are isolated", async () => { const gw = new MockGitGateway(); await gw.stage("proj-A", (await gw.status("proj-A"))[0].path); await gw.commit("proj-A", "A commit"); const logB = await gw.log("proj-B", 10); expect(logB).toHaveLength(0); }); });