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:
227
frontend/src/features/first-run/FirstRunWizard.test.tsx
Normal file
227
frontend/src/features/first-run/FirstRunWizard.test.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
/**
|
||||
* L5 — the first-run wizard wired to the stateful {@link MockProfileGateway} via
|
||||
* the real {@link DIProvider}. Covers: pre-filled editable rows, detection ✓/✗,
|
||||
* adding a custom profile, and finishing (configure ⇒ first run closed ⇒ onDone).
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
within,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
} from "@testing-library/react";
|
||||
|
||||
import { MockProfileGateway } from "@/adapters/mock";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { FirstRunWizard } from "./FirstRunWizard";
|
||||
|
||||
function renderWizard(
|
||||
profile: MockProfileGateway = new MockProfileGateway(),
|
||||
onDone = vi.fn(),
|
||||
) {
|
||||
const gateways = { profile } as unknown as Gateways;
|
||||
return {
|
||||
profile,
|
||||
onDone,
|
||||
...render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<FirstRunWizard onDone={onDone} />
|
||||
</DIProvider>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForLoaded() {
|
||||
await screen.findByLabelText("first run setup");
|
||||
}
|
||||
|
||||
describe("FirstRunWizard (with MockProfileGateway)", () => {
|
||||
it("renders the four pre-filled, editable reference profiles", async () => {
|
||||
renderWizard();
|
||||
await waitForLoaded();
|
||||
|
||||
for (const name of ["Claude Code", "OpenAI Codex CLI", "Gemini CLI", "Aider"]) {
|
||||
expect(screen.getByText(name)).toBeTruthy();
|
||||
}
|
||||
// Commands are editable inputs, pre-filled.
|
||||
const claudeCmd = screen.getByLabelText(
|
||||
"Claude Code command",
|
||||
) as HTMLInputElement;
|
||||
expect(claudeCmd.value).toBe("claude");
|
||||
});
|
||||
|
||||
it("editing a command updates the input value", async () => {
|
||||
renderWizard();
|
||||
await waitForLoaded();
|
||||
const cmd = screen.getByLabelText("Aider command") as HTMLInputElement;
|
||||
fireEvent.change(cmd, { target: { value: "aider-2" } });
|
||||
expect(cmd.value).toBe("aider-2");
|
||||
});
|
||||
|
||||
it("detection shows ✓ for claude and ✗ for the rest", async () => {
|
||||
renderWizard();
|
||||
await waitForLoaded();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Detect installed CLIs" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByLabelText("Claude Code availability").textContent,
|
||||
).toMatch(/installed/);
|
||||
});
|
||||
expect(screen.getByLabelText("Aider availability").textContent).toMatch(
|
||||
/not found/,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds a valid custom profile as a new row", async () => {
|
||||
renderWizard();
|
||||
await waitForLoaded();
|
||||
|
||||
const form = screen.getByLabelText("add custom profile");
|
||||
fireEvent.change(within(form).getByLabelText("custom name"), {
|
||||
target: { value: "My AI" },
|
||||
});
|
||||
fireEvent.change(within(form).getByLabelText("custom command"), {
|
||||
target: { value: "my-ai" },
|
||||
});
|
||||
fireEvent.click(
|
||||
within(form).getByRole("button", { name: "Add custom profile" }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText("My AI")).toBeTruthy();
|
||||
// The custom command input now exists as a row.
|
||||
expect((screen.getByLabelText("My AI command") as HTMLInputElement).value).toBe(
|
||||
"my-ai",
|
||||
);
|
||||
});
|
||||
|
||||
it("add button is disabled until the custom draft is valid", async () => {
|
||||
renderWizard();
|
||||
await waitForLoaded();
|
||||
const form = screen.getByLabelText("add custom profile");
|
||||
const addBtn = within(form).getByRole("button", {
|
||||
name: "Add custom profile",
|
||||
}) as HTMLButtonElement;
|
||||
expect(addBtn.disabled).toBe(true);
|
||||
|
||||
fireEvent.change(within(form).getByLabelText("custom name"), {
|
||||
target: { value: "X" },
|
||||
});
|
||||
fireEvent.change(within(form).getByLabelText("custom command"), {
|
||||
target: { value: "x" },
|
||||
});
|
||||
expect(addBtn.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("auto-detects on open and pre-checks only installed CLIs", async () => {
|
||||
renderWizard();
|
||||
await waitForLoaded();
|
||||
|
||||
// The mock reports only `claude` as installed → only it is pre-checked.
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
(screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
|
||||
).toBe(true),
|
||||
);
|
||||
expect(
|
||||
(screen.getByLabelText("use OpenAI Codex CLI") as HTMLInputElement).checked,
|
||||
).toBe(false);
|
||||
expect(
|
||||
(screen.getByLabelText("use Aider") as HTMLInputElement).checked,
|
||||
).toBe(false);
|
||||
// Availability was filled automatically, without clicking the button.
|
||||
expect(
|
||||
screen.getByLabelText("Claude Code availability").textContent,
|
||||
).toMatch(/installed/);
|
||||
});
|
||||
|
||||
it("finishing persists the auto-selected (installed) profiles and closes the first run", async () => {
|
||||
const { profile, onDone } = renderWizard();
|
||||
await waitForLoaded();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
(screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
|
||||
).toBe(true),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
|
||||
|
||||
await waitFor(() => expect(onDone).toHaveBeenCalled());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByLabelText("first run setup")).toBeNull(),
|
||||
);
|
||||
|
||||
// Only the installed CLI was pre-selected, so only it is persisted.
|
||||
const saved = await profile.listProfiles();
|
||||
expect(saved.map((p) => p.command)).toEqual(["claude"]);
|
||||
expect((await profile.firstRunState()).isFirstRun).toBe(false);
|
||||
});
|
||||
|
||||
it("ticking an extra profile persists it alongside the installed ones", async () => {
|
||||
const { profile } = renderWizard();
|
||||
await waitForLoaded();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
(screen.getByLabelText("use Claude Code") as HTMLInputElement).checked,
|
||||
).toBe(true),
|
||||
);
|
||||
|
||||
// Keep Aider too (not installed, unchecked by default).
|
||||
fireEvent.click(screen.getByLabelText("use Aider"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save and continue" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const saved = await profile.listProfiles();
|
||||
expect(saved.map((p) => p.command)).toEqual(["claude", "aider"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders nothing when it is not the first run", async () => {
|
||||
const profile = new MockProfileGateway();
|
||||
await profile.configureProfiles([]); // closes first run
|
||||
const { container } = renderWizard(profile);
|
||||
|
||||
// No wizard section ever appears.
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByLabelText("first run setup")).toBeNull(),
|
||||
);
|
||||
expect(container.querySelector("section")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FirstRunWizard reopening after the first run (forceOpen)", () => {
|
||||
/** A gateway whose first run is already done (profiles configured). */
|
||||
async function configuredGateway() {
|
||||
const profile = new MockProfileGateway();
|
||||
await profile.configureProfiles([]); // marks first run as done
|
||||
return profile;
|
||||
}
|
||||
|
||||
it("stays hidden once the first run is done (default)", async () => {
|
||||
const profile = await configuredGateway();
|
||||
const gateways = { profile } as unknown as Gateways;
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<FirstRunWizard />
|
||||
</DIProvider>,
|
||||
);
|
||||
// Give the async first-run state time to resolve to `false`.
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByLabelText("first run setup")).toBeNull(),
|
||||
);
|
||||
});
|
||||
|
||||
it("renders when forced open (Settings ▸ Configure profiles)", async () => {
|
||||
const profile = await configuredGateway();
|
||||
const gateways = { profile } as unknown as Gateways;
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<FirstRunWizard forceOpen />
|
||||
</DIProvider>,
|
||||
);
|
||||
expect(await screen.findByLabelText("first run setup")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user