Files
IdeA/frontend/src/features/plugins/ProjectPluginSettings.test.tsx
Blomios 1a1429f10e feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): lot utilisateur #325 #326 — icônes projet avec watermark CLI custom et localisation FR/EN (QA verte)
Lot en un seul commit: les deux features partagent les mêmes contrats
(domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes
surfaces UI — la séparation par fichiers était impossible sans chirurgie
de hunks risquant des états intermédiaires non compilables (convention
lot des batches #310-#313 et #315).

#325 — icône configurable par projet, watermark CLI custom:
- domaine: ProjectIconSet (devicon, simple-icons, lucide, idea),
  ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon
  optionnel sur ProjectSettings; sérialisation camelCase et try_from
  strict
- application: UpdateProjectSettings.execute accepte un patch
  theme_color OU icon (absent = inchangé, null = défaut produit), tests
  in-dependants avec store mémoire
- backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto
  avec icon optionnel (désérialisation additive), commandes
  update_project_settings alignées Tauri/HTTP, tests DTO et HTTP
  (défaut null, set, clear, nom invalide rejeté)
- frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en
  grille multi-lignes dans Paramètres projet, affichage de l'icône dans
  ProjectTabs et watermark grand format à faible opacité en fond de CLI
  custom; ports ProjectSettings.icon, Partial<ProjectSettings>,
  adapters tauri/mock/http et tests alignés

#326 — localisation FR/EN et traduction des textes IdeA:
- domaine: IdeaLanguage (fr | en), champ language optionnel sur
  IdeaUserSettings (absence = défaut produit, série additive validée)
- backend/web-server: UserSettingsDto.language, update_user_settings
  accepte language, tests HTTP alignés; fixtures structured_launch_d3
  mises à jour
- frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression
  des sources visibles), GeneralSettings avec sélecteur de langue dans
  les paramètres, setupI18n dans vitest; migration de l'ensemble des
  textes hardcodés vers le catalogue sur toutes les surfaces (agents,
  layout, projets, tickets, sprints, web, plugins, permissions, git,
  templates, skills, mémoire, model servers, embedder, first-run,
  devices, scheduled tasks, workstate, dialogs), FR conservé par défaut;
  recherche d'icônes #325 en anglais conformément au ticket

QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
2026-09-11 21:53:56 +02:00

209 lines
7.7 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { DIProvider } from "@/app/di";
import {
createMockGateways,
MockPluginGateway,
} from "@/adapters/mock";
import type { ProjectPluginManifest } from "@/domain";
import { SkillsPanel } from "@/features/skills";
import { McpPanel } from "@/features/permissions";
import { ProjectPluginsPanel } from "./ProjectPluginsPanel";
function manifest(): ProjectPluginManifest {
return {
ideaPluginManifestVersion: 1,
id: "dev.acme.unity",
displayName: "Unity Plugin",
publisher: "Acme",
version: "1.2.3",
main: "dist/index.js",
trustLevel: "full",
capabilities: ["ui", "mcp"],
activationScope: "app",
contributes: {
skills: [{
id: "unity.debug",
name: "unity-debug",
description: "Diagnostiquer les builds Unity.",
kind: "workflow",
path: "skills/unity-debug/SKILL.md",
}],
menus: [],
menuItems: [],
layouts: [],
mcpServers: [
{
id: "unity-tools",
displayName: "Unity Tools",
transport: "stdio",
tools: [{ name: "unity_build", description: "Build the Unity project." }],
},
{ id: "legacy-tools", displayName: "Legacy Tools", transport: "stdio" },
],
},
};
}
function gatewaysWithPlugin() {
const plugin = new MockPluginGateway();
plugin._seedProjectPluginManifest(manifest());
return { ...createMockGateways(), plugin };
}
describe("project plugin settings #282", () => {
it("lists globally installed plugins, details their capabilities and toggles project activation", async () => {
const gateways = gatewaysWithPlugin();
render(
<DIProvider gateways={gateways}>
<ProjectPluginsPanel projectId="project-1" />
</DIProvider>,
);
expect(await screen.findByText("Unity Plugin")).toBeTruthy();
expect(screen.getByText("Installé globalement")).toBeTruthy();
fireEvent.click(screen.getByText("Voir les capacités"));
expect(screen.getByText("unity-debug")).toBeTruthy();
expect(screen.getByText("Unity Tools")).toBeTruthy();
const toggle = screen.getByRole("checkbox", { name: "Activé pour ce projet" });
fireEvent.click(toggle);
await waitFor(() => expect((toggle as HTMLInputElement).checked).toBe(true));
expect((await gateways.plugin.listProjectSettings("project-1")).settings.plugins).toEqual([
{ pluginId: "dev.acme.unity", enabled: true },
]);
});
it("offers an obvious entry point to assign plugin capabilities to agents", async () => {
const gateways = gatewaysWithPlugin();
const onAssignAgents = vi.fn();
render(
<DIProvider gateways={gateways}>
<ProjectPluginsPanel
projectId="project-1"
onAssignAgents={onAssignAgents}
/>
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: "Gérer les skills" }));
expect(onAssignAgents).toHaveBeenCalledOnce();
});
it("assigns plugin skills from the unified Skills panel and keeps tombstones removable", async () => {
const gateways = gatewaysWithPlugin();
const agent = await gateways.agent.createAgent("project-1", {
name: "Dev",
profileId: "profile-1",
});
gateways.plugin._seedProjectPluginSettings("project-1", {
version: 1,
plugins: [{ pluginId: "dev.acme.unity", enabled: true }],
agents: [{
agentId: agent.id,
skills: [{ pluginId: "missing.plugin", skillId: "removed-skill" }],
tools: [],
}],
});
render(
<DIProvider gateways={gateways}>
<SkillsPanel projectId="project-1" />
</DIProvider>,
);
const skill = await screen.findByRole("checkbox", { name: /unity-debug/ });
fireEvent.click(skill);
await waitFor(() => expect((skill as HTMLInputElement).checked).toBe(true));
expect(screen.getByText(/removed-skill/).closest("div")?.textContent).toContain(
"Indisponible",
);
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
const assigned = settings.agents.find((entry) => entry.agentId === agent.id);
expect(assigned?.skills).toContainEqual({ pluginId: "dev.acme.unity", skillId: "unity.debug" });
});
it("lists catalogued plugin tools in MCP and removes free-form assignment", async () => {
const gateways = gatewaysWithPlugin();
const agent = await gateways.agent.createAgent("project-1", {
name: "Main",
profileId: "profile-1",
});
gateways.plugin._seedProjectPluginSettings("project-1", {
version: 1,
plugins: [{ pluginId: "dev.acme.unity", enabled: true }],
agents: [{ agentId: agent.id, skills: [], tools: [] }],
});
render(
<DIProvider gateways={gateways}>
<McpPanel projectId="project-1" />
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: /Main/ }));
const staticTool = await screen.findByRole("checkbox", { name: /unity_build/ });
fireEvent.click(staticTool);
await waitFor(() => expect((staticTool as HTMLInputElement).checked).toBe(true));
expect(screen.queryByPlaceholderText("Nom exact du tool")).toBeNull();
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
expect(settings.agents.find((entry) => entry.agentId === agent.id)?.tools).toContainEqual({
pluginId: "dev.acme.unity", serverId: "unity-tools", toolName: "unity_build",
});
});
it("keeps disabled and missing plugin assignments removable", async () => {
const gateways = gatewaysWithPlugin();
const agent = await gateways.agent.createAgent("project-1", {
name: "QA",
profileId: "profile-1",
});
gateways.plugin._seedProjectPluginSettings("project-1", {
version: 1,
plugins: [{ pluginId: "dev.acme.unity", enabled: false }],
agents: [{
agentId: agent.id,
skills: [{ pluginId: "missing.plugin", skillId: "gone-skill" }],
tools: [
{ pluginId: "dev.acme.unity", serverId: "unity-tools", toolName: "unity_build" },
{ pluginId: "dev.acme.unity", serverId: "legacy-tools", toolName: "legacy_ping" },
{ pluginId: "missing.plugin", serverId: "gone-server", toolName: "gone_tool" },
],
}],
});
render(
<DIProvider gateways={gateways}>
<McpPanel projectId="project-1" />
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: /QA/ }));
const disabledAssigned = await screen.findByRole("checkbox", { name: /unity_build/ });
expect((disabledAssigned as HTMLInputElement).checked).toBe(true);
expect((disabledAssigned as HTMLInputElement).disabled).toBe(false);
fireEvent.click(disabledAssigned);
await waitFor(() => expect((disabledAssigned as HTMLInputElement).checked).toBe(false));
const runtimeOnly = screen.getByText(/legacy_ping/).closest("div")!;
expect(runtimeOnly.textContent).toContain("Indisponible");
expect(screen.queryByRole("checkbox", { name: /legacy_ping/ })).toBeNull();
fireEvent.click(runtimeOnly.querySelector("button")!);
await waitFor(async () => {
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
expect(settings.agents[0]?.tools).not.toContainEqual(
expect.objectContaining({ toolName: "legacy_ping" }),
);
});
const stale = screen.getByText(/gone_tool/).closest("div")!;
fireEvent.click(stale.querySelector("button")!);
await waitFor(async () => {
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
expect(settings.agents[0]?.tools).not.toContainEqual(
expect.objectContaining({ toolName: "gone_tool" }),
);
});
});
});