feat(frontend): groupes repliables par plugin pour skills et tools MCP, bouton Joindre harmonisé avec Envoyer — nouveau composant PluginContributionGroup (en-tête cliquable aria-expanded/aria-controls, badge Chargé/Non chargé, compteur de contributions, replié par défaut pour les plugins non loadés, dépliage forcé pendant la recherche) adopté par SkillsPanel et McpToolPermissionsPanel avec déduplication des plugins — #367 ; bouton Joindre du chat aligné sur le format d'Envoyer (size md, variante secondary, état loading « Ajout… », aria-label dédié, conteneur custom-agent-chat-composer-actions avec data-busy, Stop aligné md, container queries 419.99px en colonne et 279.99px wrap puis pleine largeur en busy) — #368 (QA verte: npm test ciblé ProjectPluginSettings + skills + McpToolPermissionsPanel + CustomAgentChatView + visibleSourceScan 5 fichiers 152 tests OK, Firefox headless layout 600/360/240 et busy 240 PASS, npm run build OK, git diff --check OK)
This commit is contained in:
@ -212,6 +212,7 @@ describe("CustomAgentChatView", () => {
|
||||
{ width: 240, mode: "full-width" },
|
||||
])("keeps its conversation access clear of cell controls at $width px ($mode)", ({ width, mode }) => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
|
||||
sendAgentChat: vi.fn(),
|
||||
cancelAgentChat: vi.fn(),
|
||||
@ -263,6 +264,104 @@ describe("CustomAgentChatView", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ width: 600, layout: "inline" },
|
||||
{ width: 360, layout: "equal-width-row" },
|
||||
{ width: 240, layout: "narrow-wrap" },
|
||||
])("keeps composer actions usable and visually aligned at $width px ($layout)", async ({ width, layout }) => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
|
||||
sendAgentChat: vi.fn(),
|
||||
cancelAgentChat: vi.fn(),
|
||||
closeAgentChat: vi.fn(),
|
||||
};
|
||||
|
||||
render(
|
||||
<div data-testid="composer-cell" style={{ width, height: 640 }}>
|
||||
<DIProvider gateways={{ agent, system: { pickFile: vi.fn(async () => null) } } as unknown as Gateways}>
|
||||
<CustomAgentChatView
|
||||
projectId="project-1"
|
||||
agentId="agent-1"
|
||||
agentName="Worker"
|
||||
profile={profile}
|
||||
cwd="/repo"
|
||||
nodeId="node-1"
|
||||
sessionId="chat-1"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const attach = screen.getByRole("button", { name: "Joindre un fichier" });
|
||||
const send = screen.getByRole("button", { name: "Envoyer" });
|
||||
const actions = attach.parentElement!;
|
||||
const composer = actions.parentElement!;
|
||||
expect(screen.getByTestId("composer-cell").style.width).toBe(`${width}px`);
|
||||
expect(attach.textContent).toBe("Joindre");
|
||||
expect(attach.className).toContain("min-h-11");
|
||||
expect(attach.className).toContain("border-border");
|
||||
expect(send.className).toContain("min-h-11");
|
||||
expect(send.className).toContain("bg-primary");
|
||||
await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(false));
|
||||
expect((send as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(actions.className).toContain("custom-agent-chat-composer-actions");
|
||||
expect(composer.className).toContain("custom-agent-chat-composer");
|
||||
|
||||
if (layout === "equal-width-row") {
|
||||
expect(themeCss).toContain(".custom-agent-chat-composer-action {\n flex: 1 1 0;");
|
||||
} else if (layout === "narrow-wrap") {
|
||||
expect(themeCss).toContain(".custom-agent-chat-composer-actions {\n flex-wrap: wrap;");
|
||||
expect(themeCss).toContain('.custom-agent-chat-composer-actions[data-busy="true"]');
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps file selection behavior while exposing its in-progress state", async () => {
|
||||
const picked = deferred<string | null>();
|
||||
const pickFile = vi.fn(() => picked.promise);
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
|
||||
sendAgentChat: vi.fn(),
|
||||
cancelAgentChat: vi.fn(),
|
||||
closeAgentChat: vi.fn(),
|
||||
};
|
||||
render(
|
||||
<DIProvider gateways={{ agent, system: { pickFile } } as unknown as Gateways}>
|
||||
<CustomAgentChatView
|
||||
projectId="project-1"
|
||||
agentId="agent-1"
|
||||
agentName="Worker"
|
||||
profile={profile}
|
||||
cwd="/repo"
|
||||
nodeId="node-1"
|
||||
sessionId="chat-1"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(agent.reattachAgentChat).toHaveBeenCalledWith("chat-1", expect.any(Function)),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect((screen.getByRole("button", { name: "Joindre un fichier" }) as HTMLButtonElement).disabled).toBe(false),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Joindre un fichier" }));
|
||||
expect(pickFile).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole("button", { name: "Joindre un fichier" }).textContent).toContain("Ajout…");
|
||||
expect((screen.getByRole("button", { name: "Joindre un fichier" }) as HTMLButtonElement).disabled).toBe(true);
|
||||
|
||||
await act(async () => picked.resolve("/tmp/report.pdf"));
|
||||
expect(await screen.findByText("Fichier joint: report.pdf")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Joindre un fichier" }).textContent).toBe("Joindre");
|
||||
});
|
||||
|
||||
it("renders the project icon as a non-interactive custom-CLI watermark and updates it immediately", async () => {
|
||||
const agent = {
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [] })),
|
||||
|
||||
Reference in New Issue
Block a user