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:
136
frontend/src/features/layout/LayoutTabs.test.tsx
Normal file
136
frontend/src/features/layout/LayoutTabs.test.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* #4 — LayoutTabs / useLayouts: named-layout tab bar driven through the
|
||||
* MockLayoutGateway. Covers: list, create, rename, delete, switch.
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent, act } from "@testing-library/react";
|
||||
|
||||
import type { Gateways } from "@/ports";
|
||||
import { MockLayoutGateway, MockAgentGateway, MockTerminalGateway } from "@/adapters/mock";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { LayoutTabs } from "./LayoutTabs";
|
||||
|
||||
function renderTabs(
|
||||
layout: MockLayoutGateway,
|
||||
projectId = "p1",
|
||||
onActiveLayoutChange = vi.fn(),
|
||||
) {
|
||||
const gateways = {
|
||||
layout,
|
||||
terminal: new MockTerminalGateway(),
|
||||
agent: new MockAgentGateway(),
|
||||
} as unknown as Gateways;
|
||||
return render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<LayoutTabs projectId={projectId} onActiveLayoutChange={onActiveLayoutChange} />
|
||||
</DIProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("LayoutTabs", () => {
|
||||
it("renders the default 'Default' tab on first load", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
renderTabs(layout);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Default")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a create (+) button", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
renderTabs(layout);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("create layout")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("switching tabs reports the new active layout (id + kind) to the parent", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
// Seed a second layout.
|
||||
await layout.createLayout("p1", "Beta");
|
||||
const onActiveLayoutChange = vi.fn();
|
||||
renderTabs(layout, "p1", onActiveLayoutChange);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Beta")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click the Beta tab.
|
||||
fireEvent.click(screen.getByText("Beta"));
|
||||
|
||||
await waitFor(() => {
|
||||
// Called with the full LayoutInfo (so the parent knows the kind too).
|
||||
expect(onActiveLayoutChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: "Beta", kind: "terminal" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("renaming a tab (double-click → type → blur) updates the name", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
renderTabs(layout);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Default")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Double-click to start rename.
|
||||
fireEvent.dblClick(screen.getByText("Default"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/rename layout/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
const input = screen.getByLabelText(/rename layout/i) as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: "My Layout" } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("My Layout")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("delete button is disabled when only one layout exists", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
renderTabs(layout);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/delete layout/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
const closeBtn = screen.getByLabelText(/delete layout/i) as HTMLButtonElement;
|
||||
expect(closeBtn.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("creating a layout adds a tab", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
// Directly create a second layout via the gateway (simulates the + flow).
|
||||
await layout.createLayout("p1", "Extra");
|
||||
renderTabs(layout);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Default")).toBeTruthy();
|
||||
expect(screen.getByText("Extra")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("deleting a layout removes its tab (when 2 exist)", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
await layout.createLayout("p1", "Second");
|
||||
renderTabs(layout);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Second")).toBeTruthy();
|
||||
});
|
||||
|
||||
const deleteBtns = screen.getAllByLabelText(/delete layout/i);
|
||||
// Both tabs exist, pick the second (Second).
|
||||
await act(async () => {
|
||||
fireEvent.click(deleteBtns[1]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Second")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user