Files
IdeaSDK/frontend/src/features/projects/ProjectsView.ls7.test.tsx
Blomios c179f93a26 feat(frontend): boutons cancel/retry des tâches de fond (B8)
Câble l'action utilisateur sur les commandes Tauri B8 :

- port WorkStateGateway : ajout de cancelBackgroundTask/retryBackgroundTask
  (le read-model se rafraîchit via l'événement domaine backgroundTaskChanged ;
  retry rejoue sous un NOUVEL id de tâche).
- adapter Tauri : invoke cancel_background_task/retry_background_task.
- adapter mock : no-ops (le refresh réel est piloté par les événements).
- ProjectWorkStatePanel (BackgroundTaskRow) : état busy + affichage d'erreur
  + refresh après action.
- test ProjectsView.ls7 : stub de gateway complété.

Build vert : npm run build (tsc --noEmit + vite build), vitest 449 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:53:32 +02:00

213 lines
6.8 KiB
TypeScript

/**
* LS7 — `ProjectsView` integration: clicking a conversation in the Work panel swaps
* the terminal grid for the read-only `ConversationViewer`; the back button returns
* to the grid; switching project resets the viewer; and with no conversation open
* the terminal grid is shown unchanged (non-regression).
*/
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import {
MockAgentGateway,
MockGitGateway,
MockLayoutGateway,
MockProfileGateway,
MockProjectGateway,
MockSystemGateway,
MockTemplateGateway,
MockTerminalGateway,
} from "@/adapters/mock";
import type {
ConversationGateway,
Gateways,
WorkStateGateway,
} from "@/ports";
import type { ProjectWorkState } from "@/domain";
import { DIProvider } from "@/app/di";
import { ProjectsView } from "./ProjectsView";
const CONV_ID = "conversation-open-me";
/** A work-state gateway that returns a fixed, clickable conversation for any project. */
function fixedWorkState(): WorkStateGateway {
const state: ProjectWorkState = {
agents: [
{
agentId: "agent-1",
name: "Worker",
profileId: "codex",
busy: { state: "idle" },
tickets: [
{
ticketId: "ticket-1",
conversationId: CONV_ID,
position: 0,
status: "inProgress",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: "do the thing",
taskLen: 12,
},
],
},
],
conversations: [
{
conversationId: CONV_ID,
status: "ready",
objectivePreview: "Objectif du fil",
summaryPreview: "Résumé du fil",
summaryLen: 13,
upTo: "turn-1",
recentTurns: [],
},
],
};
return {
getProjectWorkState: async () => structuredClone(state),
cancelBackgroundTask: async () => {},
retryBackgroundTask: async () => {},
};
}
/** A conversation gateway that returns one identifiable turn for any thread. */
function fixedConversation(): ConversationGateway {
return {
readPage: async () => ({
turns: [
{
id: "t1",
atMs: 1_700_000_000_000,
role: "prompt",
source: { kind: "human" },
text: "contenu du fil ouvert",
textLen: 21,
},
],
hasMore: false,
}),
};
}
function renderView(project: MockProjectGateway) {
const agent = new MockAgentGateway();
const gateways = {
system: new MockSystemGateway(),
project,
agent,
profile: new MockProfileGateway(),
template: new MockTemplateGateway(agent),
git: new MockGitGateway(),
layout: new MockLayoutGateway(),
terminal: new MockTerminalGateway(),
workState: fixedWorkState(),
conversation: fixedConversation(),
} as unknown as Gateways;
return render(
<DIProvider gateways={gateways}>
<ProjectsView />
</DIProvider>,
);
}
async function openProjectAndWorkTab(label: string) {
await screen.findByText(label);
// Open the project whose root is `label`.
const li = screen
.getAllByRole("listitem")
.find((node) => within(node).queryByText(label));
fireEvent.click(within(li!).getByRole("button", { name: "Open" }));
await screen.findByRole("tab");
// Switch the sidebar to the Work panel.
fireEvent.click(screen.getByRole("button", { name: "Work" }));
}
describe("ProjectsView — LS7 conversation viewer integration", () => {
it("opens the viewer on a conversation click and returns to the grid on back", async () => {
const project = new MockProjectGateway();
await project.createProject("alpha", "/p/a");
renderView(project);
await openProjectAndWorkTab("/p/a");
// Before: the terminal grid owns the main area (non-regression baseline).
expect(await screen.findByTestId("layout-grid")).toBeTruthy();
expect(
screen.queryByRole("button", { name: "← Retour aux terminaux" }),
).toBeNull();
// Click the conversation row in the Work panel.
const open = await screen.findByRole("button", {
name: `open conversation ${CONV_ID}`,
});
fireEvent.click(open);
// After: the viewer replaced the grid in the main area.
expect(
await screen.findByRole("button", { name: "← Retour aux terminaux" }),
).toBeTruthy();
expect(await screen.findByText("contenu du fil ouvert")).toBeTruthy();
expect(screen.queryByTestId("layout-grid")).toBeNull();
// Back ⇒ the terminal grid is restored, the viewer is gone.
fireEvent.click(
screen.getByRole("button", { name: "← Retour aux terminaux" }),
);
expect(await screen.findByTestId("layout-grid")).toBeTruthy();
expect(
screen.queryByRole("button", { name: "← Retour aux terminaux" }),
).toBeNull();
});
it("resets the viewer when the active project changes", async () => {
const project = new MockProjectGateway();
await project.createProject("alpha", "/p/a");
await project.createProject("beta", "/p/b");
renderView(project);
// Open BOTH projects as tabs (while the Projects sidebar list is visible).
await screen.findByText("/p/a");
for (const root of ["/p/a", "/p/b"]) {
const li = screen
.getAllByRole("listitem")
.find((node) => within(node).queryByText(root));
fireEvent.click(within(li!).getByRole("button", { name: "Open" }));
}
await waitFor(() => expect(screen.getAllByRole("tab")).toHaveLength(2));
// Make alpha the active tab, then open its conversation viewer.
fireEvent.click(screen.getByRole("tab", { name: "alpha" }));
fireEvent.click(screen.getByRole("button", { name: "Work" }));
fireEvent.click(
await screen.findByRole("button", { name: `open conversation ${CONV_ID}` }),
);
await screen.findByRole("button", { name: "← Retour aux terminaux" });
// Switch the active project (top tab) ⇒ viewer resets to the grid.
fireEvent.click(screen.getByRole("tab", { name: "beta" }));
await waitFor(() =>
expect(
screen.queryByRole("button", { name: "← Retour aux terminaux" }),
).toBeNull(),
);
expect(await screen.findByTestId("layout-grid")).toBeTruthy();
});
it("shows the terminal grid unchanged when no conversation is open (non-regression)", async () => {
const project = new MockProjectGateway();
await project.createProject("alpha", "/p/a");
renderView(project);
await screen.findByText("/p/a");
fireEvent.click(screen.getByRole("button", { name: "Open" }));
await screen.findByRole("tab");
// No conversation opened ⇒ the main area is the terminal grid, no viewer.
expect(await screen.findByTestId("layout-grid")).toBeTruthy();
expect(
screen.queryByRole("button", { name: "← Retour aux terminaux" }),
).toBeNull();
});
});