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.
This commit is contained in:
@ -16,6 +16,7 @@ import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import type { Skill, SkillScope } from "@/domain";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { Button, Input, cn } from "@/shared";
|
||||
|
||||
export interface SkillEditorProps {
|
||||
@ -40,6 +41,7 @@ export function SkillEditor({
|
||||
onClose,
|
||||
busy = false,
|
||||
}: SkillEditorProps) {
|
||||
const { tr } = useI18n();
|
||||
const [name, setName] = useState(skill?.name ?? "");
|
||||
const [content, setContent] = useState(skill?.contentMd ?? "");
|
||||
const [scope, setScope] = useState<SkillScope>(skill?.scope ?? "project");
|
||||
@ -59,17 +61,17 @@ export function SkillEditor({
|
||||
className="fixed inset-0 z-50 flex flex-col bg-canvas"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="skill editor"
|
||||
aria-label={tr("Éditeur de skill", "Skill editor")}
|
||||
>
|
||||
{/* ── Header bar ── */}
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-border bg-surface px-4 py-3">
|
||||
<h2 className="text-sm font-semibold text-content">
|
||||
{editing ? `Edit skill — ${skill.name}` : "New skill"}
|
||||
{editing ? tr(`Modifier le skill — ${skill.name}`, `Edit skill — ${skill.name}`) : tr("Nouveau skill", "New skill")}
|
||||
</h2>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
aria-label="close skill editor"
|
||||
aria-label={tr("Fermer l’éditeur de skill", "Close skill editor")}
|
||||
onClick={onClose}
|
||||
disabled={busy}
|
||||
>
|
||||
@ -86,12 +88,12 @@ export function SkillEditor({
|
||||
<div className="flex shrink-0 flex-wrap items-end gap-3 border-b border-border px-4 py-3">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<label htmlFor="se-name" className="text-xs font-medium text-muted">
|
||||
Name
|
||||
{tr("Nom", "Name")}
|
||||
</label>
|
||||
<Input
|
||||
id="se-name"
|
||||
aria-label="skill name"
|
||||
placeholder="My skill"
|
||||
aria-label={tr("Nom du skill", "Skill name")}
|
||||
placeholder={tr("Mon skill", "My skill")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={busy}
|
||||
@ -101,11 +103,11 @@ export function SkillEditor({
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<label htmlFor="se-scope" className="text-xs font-medium text-muted">
|
||||
Scope
|
||||
{tr("Portée", "Scope")}
|
||||
</label>
|
||||
<select
|
||||
id="se-scope"
|
||||
aria-label="skill scope"
|
||||
aria-label={tr("Portée du skill", "Skill scope")}
|
||||
value={scope}
|
||||
onChange={(e) => setScope(e.target.value as SkillScope)}
|
||||
disabled={busy || editing}
|
||||
@ -115,8 +117,8 @@ export function SkillEditor({
|
||||
"focus:border-primary disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
<option value="project">Project</option>
|
||||
<option value="global">Global</option>
|
||||
<option value="project">{tr("Projet", "Project")}</option>
|
||||
<option value="global">{tr("Global", "Global")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -128,7 +130,7 @@ export function SkillEditor({
|
||||
key={t}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-label={t === "edit" ? "Edit" : "Preview"}
|
||||
aria-label={t === "edit" ? tr("Modifier", "Edit") : tr("Aperçu", "Preview")}
|
||||
aria-selected={tab === t}
|
||||
onClick={() => setTab(t)}
|
||||
className={cn(
|
||||
@ -138,7 +140,7 @@ export function SkillEditor({
|
||||
: "text-muted hover:text-content",
|
||||
)}
|
||||
>
|
||||
{t === "edit" ? "Edit" : "Preview"}
|
||||
{t === "edit" ? tr("Modifier", "Edit") : tr("Aperçu", "Preview")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -147,7 +149,7 @@ export function SkillEditor({
|
||||
<div className="flex flex-1 flex-col overflow-hidden px-4 py-3">
|
||||
{tab === "edit" ? (
|
||||
<textarea
|
||||
aria-label="skill content"
|
||||
aria-label={tr("Contenu du skill", "Skill content")}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
disabled={busy}
|
||||
@ -157,7 +159,7 @@ export function SkillEditor({
|
||||
"focus:border-primary placeholder:text-faint",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
placeholder="# Skill workflow ..."
|
||||
placeholder={tr("# Déroulé du skill\n\n…", "# Skill workflow\n\n…")}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@ -179,7 +181,7 @@ export function SkillEditor({
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<p className="text-sm text-muted italic">
|
||||
Nothing to preview yet.
|
||||
{tr("Rien à prévisualiser pour le moment.", "Nothing to preview yet.")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -189,16 +191,16 @@ export function SkillEditor({
|
||||
{/* ── Footer ── */}
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-border px-4 py-3">
|
||||
<Button type="button" variant="ghost" onClick={onClose} disabled={busy}>
|
||||
Cancel
|
||||
{tr("Annuler", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
aria-label="Save skill"
|
||||
aria-label={tr("Enregistrer le skill", "Save skill")}
|
||||
disabled={!canSave}
|
||||
loading={busy}
|
||||
>
|
||||
Save
|
||||
{tr("Enregistrer", "Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -19,6 +19,7 @@ import { useGateways } from "@/app/di";
|
||||
import type { Agent, PluginSkillRef, Skill, SkillRef, SkillScope } from "@/domain";
|
||||
import { useProjectPlugins } from "@/features/plugins/useProjectPlugins";
|
||||
import { Button, Input, Panel, Spinner, cn } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { SkillEditor } from "./SkillEditor";
|
||||
import { useSkills } from "./useSkills";
|
||||
|
||||
@ -34,6 +35,7 @@ const nativeSkillKey = (ref: SkillRef) => `${ref.scope}:${ref.skillId}`;
|
||||
const pluginSkillKey = (ref: PluginSkillRef) => `${ref.pluginId}:${ref.skillId}`;
|
||||
|
||||
export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
const { tr } = useI18n();
|
||||
const vm = useSkills(projectId);
|
||||
const { agent: agentGateway, skill: skillGateway } = useGateways();
|
||||
const plugins = useProjectPlugins(projectId);
|
||||
@ -145,7 +147,7 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
{skills.length === 0 ? (
|
||||
<p className="text-sm text-muted">No {scope} skills yet.</p>
|
||||
<p className="text-sm text-muted">{scope === "project" ? tr("Aucun skill de projet pour le moment.", "No project skills yet.") : tr("Aucun skill global pour le moment.", "No global skills yet.")}</p>
|
||||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{skills.map((s) => (
|
||||
@ -158,21 +160,21 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={`edit skill ${s.name}`}
|
||||
aria-label={tr(`Modifier le skill ${s.name}`, `Edit skill ${s.name}`)}
|
||||
disabled={vm.busy}
|
||||
onClick={() => setEditorState({ mode: "edit", skill: s })}
|
||||
>
|
||||
Edit
|
||||
{tr("Modifier", "Edit")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={`delete skill ${s.name}`}
|
||||
aria-label={tr(`Supprimer le skill ${s.name}`, `Delete skill ${s.name}`)}
|
||||
disabled={vm.busy}
|
||||
onClick={() => void vm.deleteSkill(s.scope, s.id)}
|
||||
className="text-danger hover:text-danger"
|
||||
>
|
||||
Delete
|
||||
{tr("Supprimer", "Delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
@ -195,7 +197,7 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Panel title="Skills" className="flex flex-col gap-0">
|
||||
<Panel title={tr("Skills", "Skills")} className="flex flex-col gap-0">
|
||||
{(vm.error || plugins.error || assignmentError) && (
|
||||
<p
|
||||
role="alert"
|
||||
@ -206,10 +208,10 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
)}
|
||||
|
||||
<div className="flex min-h-0 flex-col gap-3 border-b border-border p-4 md:flex-row">
|
||||
<nav aria-label="cibles des skills" className="flex w-full shrink-0 flex-col gap-1.5 md:w-56">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-faint">Agents</h4>
|
||||
<nav aria-label={tr("Cibles des skills", "Skill targets")} className="flex w-full shrink-0 flex-col gap-1.5 md:w-56">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-faint">{tr("Agents", "Agents")}</h4>
|
||||
{agents.length === 0 ? (
|
||||
<p className="text-sm text-muted">Aucun agent dans ce projet.</p>
|
||||
<p className="text-sm text-muted">{tr("Aucun agent dans ce projet.", "No agents in this project.")}</p>
|
||||
) : agents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
@ -228,21 +230,21 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<section className="min-w-0 flex-1" aria-label="attribution des skills">
|
||||
<section className="min-w-0 flex-1" aria-label={tr("Attribution des skills", "Skill assignment")}>
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-content">
|
||||
{selectedAgent ? `Skills — ${selectedAgent.name}` : "Skills"}
|
||||
{selectedAgent ? `${tr("Skills", "Skills")} — ${selectedAgent.name}` : tr("Skills", "Skills")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted">Skills IdeA et plugins disponibles.</p>
|
||||
<p className="text-xs text-muted">{tr("Skills IdeA et plugins disponibles.", "Available IdeA and plugin skills.")}</p>
|
||||
</div>
|
||||
{(assignmentBusy || plugins.busy || plugins.loading) && <Spinner size={14} />}
|
||||
</div>
|
||||
<Input
|
||||
aria-label="rechercher un skill"
|
||||
aria-label={tr("Rechercher un skill", "Search skills")}
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Rechercher un skill"
|
||||
placeholder={tr("Rechercher un skill", "Search skills")}
|
||||
disabled={!selectedAgent}
|
||||
/>
|
||||
{selectedAgent && (
|
||||
@ -254,18 +256,20 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
<label key={`${skill.scope}:${skill.id}`} className="flex items-start gap-2 rounded bg-raised px-3 py-2 text-xs text-content">
|
||||
<input type="checkbox" checked={assigned} disabled={assignmentBusy || vm.busy} onChange={(event) => void setNativeSkill(skill, event.target.checked)} />
|
||||
<span className="min-w-0 flex-1"><span className="font-medium">{skill.name}</span>{skill.description ? ` — ${skill.description}` : ""}</span>
|
||||
<span className="text-faint">{skill.scope === "project" ? "Projet" : "Global"}</span>
|
||||
<span className="text-faint">{skill.scope === "project" ? tr("Projet", "Project") : tr("Global", "Global")}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{staleNativeSkills.filter((ref) => matches(ref.skillId)).map((ref) => (
|
||||
<div key={`missing:${nativeSkillKey(ref)}`} className="flex items-center justify-between gap-2 rounded border border-warning/40 px-3 py-2 text-xs">
|
||||
<span><span className="font-medium">{ref.skillId}</span> <span className="text-warning">— Indisponible</span> <span className="text-faint">IdeA</span></span>
|
||||
<Button size="sm" variant="ghost" disabled={assignmentBusy} onClick={() => void removeNativeSkill(ref)}>Retirer</Button>
|
||||
<span><span className="font-medium">{ref.skillId}</span> <span className="text-warning">— {tr("Indisponible", "Unavailable")}</span> <span className="text-faint">IdeA</span></span>
|
||||
<Button size="sm" variant="ghost" disabled={assignmentBusy} onClick={() => void removeNativeSkill(ref)}>{tr("Retirer", "Remove")}</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<h5 className="mt-2 text-xs font-semibold uppercase tracking-wide text-faint">Plugins</h5>
|
||||
<h5 className="mt-2 text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
{tr("Plugins", "Plugins")}
|
||||
</h5>
|
||||
{plugins.installed.flatMap((plugin) =>
|
||||
(plugin.contributes.skills ?? [])
|
||||
.filter((skill) => matches(`${skill.name} ${skill.description} ${plugin.displayName}`))
|
||||
@ -275,7 +279,7 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
return (
|
||||
<label key={`${plugin.id}:${skill.id}`} className="flex items-start gap-2 rounded bg-raised px-3 py-2 text-xs text-content">
|
||||
<input type="checkbox" checked={assigned} disabled={plugins.busy || (!enabled && !assigned)} onChange={(event) => void plugins.setSkillAssigned(selectedAgent.id, plugin.id, skill.id, event.target.checked)} />
|
||||
<span className="min-w-0 flex-1"><span className="font-medium">{skill.name}</span>{skill.description ? ` — ${skill.description}` : ""}{!enabled && <span className="text-warning"> — Plugin désactivé</span>}</span>
|
||||
<span className="min-w-0 flex-1"><span className="font-medium">{skill.name}</span>{skill.description ? ` — ${skill.description}` : ""}{!enabled && <span className="text-warning"> — {tr("Plugin désactivé", "Plugin disabled")}</span>}</span>
|
||||
<span className="text-faint">{plugin.displayName}</span>
|
||||
</label>
|
||||
);
|
||||
@ -283,12 +287,12 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
)}
|
||||
{stalePluginSkills.filter((ref) => matches(`${ref.pluginId} ${ref.skillId}`)).map((ref) => (
|
||||
<div key={`missing-plugin:${pluginSkillKey(ref)}`} className="flex items-center justify-between gap-2 rounded border border-warning/40 px-3 py-2 text-xs">
|
||||
<span><span className="font-medium">{ref.skillId}</span> <span className="text-warning">— Indisponible</span> <span className="text-faint">Plugin: {ref.pluginId}</span></span>
|
||||
<Button size="sm" variant="ghost" disabled={plugins.busy} onClick={() => void plugins.setSkillAssigned(selectedAgent.id, ref.pluginId, ref.skillId, false)}>Retirer</Button>
|
||||
<span><span className="font-medium">{ref.skillId}</span> <span className="text-warning">— {tr("Indisponible", "Unavailable")}</span> <span className="text-faint">{tr("Plugin", "Plugin")}: {ref.pluginId}</span></span>
|
||||
<Button size="sm" variant="ghost" disabled={plugins.busy} onClick={() => void plugins.setSkillAssigned(selectedAgent.id, ref.pluginId, ref.skillId, false)}>{tr("Retirer", "Remove")}</Button>
|
||||
</div>
|
||||
))}
|
||||
{nativeSkills.length === 0 && plugins.installed.every((plugin) => (plugin.contributes.skills ?? []).length === 0) && staleNativeSkills.length === 0 && stalePluginSkills.length === 0 && (
|
||||
<p className="text-sm text-muted">Aucun skill disponible.</p>
|
||||
<p className="text-sm text-muted">{tr("Aucun skill disponible.", "No skills available.")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -298,18 +302,18 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
{/* ── Create button ── */}
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
Skills
|
||||
{tr("Skills", "Skills")}
|
||||
</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
{vm.busy && <Spinner size={14} />}
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
aria-label="create skill"
|
||||
aria-label={tr("Créer un skill", "Create skill")}
|
||||
disabled={vm.busy}
|
||||
onClick={() => setEditorState({ mode: "create" })}
|
||||
>
|
||||
New skill
|
||||
{tr("Nouveau skill", "New skill")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -317,7 +321,7 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
{/* ── Project skills ── */}
|
||||
<div className="border-b border-border px-4 pt-3">
|
||||
<h5 className="text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
Project
|
||||
{tr("Projet", "Project")}
|
||||
</h5>
|
||||
</div>
|
||||
{renderList("project", vm.projectSkills)}
|
||||
@ -325,7 +329,7 @@ export function SkillsPanel({ projectId }: SkillsPanelProps) {
|
||||
{/* ── Global skills ── */}
|
||||
<div className="border-y border-border px-4 pt-3">
|
||||
<h5 className="text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
Global
|
||||
{tr("Global", "Global")}
|
||||
</h5>
|
||||
</div>
|
||||
{renderList("global", vm.globalSkills)}
|
||||
|
||||
@ -43,7 +43,7 @@ function renderSkillsPanel(agent?: MockAgentGateway, skill?: MockSkillGateway) {
|
||||
|
||||
async function waitForSkillsIdle() {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "create skill" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Créer un skill" })).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
@ -54,18 +54,18 @@ async function createSkill(
|
||||
scope: "project" | "global" = "project",
|
||||
) {
|
||||
await waitForSkillsIdle();
|
||||
fireEvent.click(screen.getByRole("button", { name: "create skill" }));
|
||||
await waitFor(() => expect(screen.getByLabelText("skill name")).toBeTruthy());
|
||||
fireEvent.change(screen.getByLabelText("skill name"), {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Créer un skill" }));
|
||||
await waitFor(() => expect(screen.getByLabelText("Nom du skill")).toBeTruthy());
|
||||
fireEvent.change(screen.getByLabelText("Nom du skill"), {
|
||||
target: { value: name },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("skill content"), {
|
||||
fireEvent.change(screen.getByLabelText("Contenu du skill"), {
|
||||
target: { value: content },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("skill scope"), {
|
||||
fireEvent.change(screen.getByLabelText("Portée du skill"), {
|
||||
target: { value: scope },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save skill" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Enregistrer le skill" }));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -76,8 +76,8 @@ describe("SkillsPanel (with MockSkillGateway)", () => {
|
||||
it("shows empty state for both scopes initially", async () => {
|
||||
renderSkillsPanel();
|
||||
await waitForSkillsIdle();
|
||||
expect(screen.getByText("No project skills yet.")).toBeTruthy();
|
||||
expect(screen.getByText("No global skills yet.")).toBeTruthy();
|
||||
expect(screen.getByText("Aucun skill de projet pour le moment.")).toBeTruthy();
|
||||
expect(screen.getByText("Aucun skill global pour le moment.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creating a project skill adds it to the list", async () => {
|
||||
@ -100,12 +100,12 @@ describe("SkillsPanel (with MockSkillGateway)", () => {
|
||||
it("Save is disabled when name or content is empty", async () => {
|
||||
renderSkillsPanel();
|
||||
await waitForSkillsIdle();
|
||||
fireEvent.click(screen.getByRole("button", { name: "create skill" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Créer un skill" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("button", { name: "Save skill" })).toBeTruthy(),
|
||||
expect(screen.getByRole("button", { name: "Enregistrer le skill" })).toBeTruthy(),
|
||||
);
|
||||
const save = screen.getByRole("button", {
|
||||
name: "Save skill",
|
||||
name: "Enregistrer le skill",
|
||||
}) as HTMLButtonElement;
|
||||
expect(save.disabled).toBe(true);
|
||||
});
|
||||
@ -115,14 +115,14 @@ describe("SkillsPanel (with MockSkillGateway)", () => {
|
||||
await createSkill("Editable", "old", "project");
|
||||
await screen.findByText("Editable");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "edit skill Editable" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Modifier le skill Editable" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText("skill content")).toBeTruthy(),
|
||||
expect(screen.getByLabelText("Contenu du skill")).toBeTruthy(),
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("skill content"), {
|
||||
fireEvent.change(screen.getByLabelText("Contenu du skill"), {
|
||||
target: { value: "new content" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save skill" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Enregistrer le skill" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const list = await skill.listSkills(PROJECT_ID, "project");
|
||||
@ -135,7 +135,7 @@ describe("SkillsPanel (with MockSkillGateway)", () => {
|
||||
await createSkill("ToDelete", "x", "project");
|
||||
await screen.findByText("ToDelete");
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "delete skill ToDelete" }),
|
||||
screen.getByRole("button", { name: "Supprimer le skill ToDelete" }),
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByText("ToDelete")).toBeNull());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user