feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
284
frontend/src/features/git/git.test.tsx
Normal file
284
frontend/src/features/git/git.test.tsx
Normal file
@ -0,0 +1,284 @@
|
||||
/**
|
||||
* L8 — git feature wired to the stateful `MockGitGateway` via the real
|
||||
* `DIProvider`.
|
||||
*
|
||||
* Covers:
|
||||
* - status shows staged/unstaged groupings
|
||||
* - stage 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(
|
||||
<DIProvider gateways={gateways}>
|
||||
<GitPanel projectId={PROJECT_ID} />
|
||||
</DIProvider>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** Waits until the "Changes" section is rendered (panel stabilised). */
|
||||
async function waitForPanel() {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("region", { name: "Changes" })).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();
|
||||
|
||||
expect(screen.getByText("Staged")).toBeTruthy();
|
||||
expect(screen.getByText("Unstaged")).toBeTruthy();
|
||||
// src/main.rs is staged → Unstage button exists
|
||||
expect(
|
||||
screen.getByRole("button", { name: "unstage src/main.rs" }),
|
||||
).toBeTruthy();
|
||||
// README.md is unstaged → Stage button exists
|
||||
expect(
|
||||
screen.getByRole("button", { name: "stage 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: "stage README.md",
|
||||
});
|
||||
fireEvent.click(stageBtn);
|
||||
|
||||
// After staging, an Unstage button should appear for README.md
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: "unstage README.md" }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("clicking Unstage 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: "unstage src/main.rs",
|
||||
});
|
||||
fireEvent.click(unstageBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: "stage 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("commit message"), {
|
||||
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("commit message"), {
|
||||
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("No changes.")).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("new branch name");
|
||||
fireEvent.change(branchInput, { target: { value: "feature/new-thing" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /create.*checkout/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: "checkout main" }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows 'No commits yet.' in the Log section initially", async () => {
|
||||
renderPanel();
|
||||
await waitForPanel();
|
||||
|
||||
expect(screen.getByText("No commits yet.")).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("stage 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("unstage 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user