Files
fusion/packages/dashboard/app/components/__tests__/SkillMultiselect.test.tsx
gsxdsm 9a9e591b72 FN-9115: clarify and expand agent skill selection
Clarify automatic skill availability while making forced-reading selections easier to manage.

- Replace the skill picker with a searchable multi-select checkbox list and explicit loading, error, empty, and unavailable states.
- Label forced, disabled, and undiscovered skills consistently across agent detail and list views, including a clear None state.
- Update localized guidance, documentation, regression coverage, and the published package changeset.

Files changed:
 .changeset/fn-9115-skills-ui.md                    |   7 +
 docs/dashboard-guide.md                            |   4 +
 .../agent-detail-settings-theme-styling.test.ts    |   3 +-
 .../dashboard/app/components/AgentDetailView.css   |   7 +-
 .../dashboard/app/components/AgentDetailView.tsx   |  35 ++-
 packages/dashboard/app/components/AgentsView.css   |   4 +
 packages/dashboard/app/components/AgentsView.tsx   |  13 +-
 .../dashboard/app/components/NewAgentDialog.tsx    |   5 +-
 .../dashboard/app/components/SkillMultiselect.css  | 258 +++------------------
 .../dashboard/app/components/SkillMultiselect.tsx  | 178 +++++---------
 .../AgentDetailView.mobile-scroll.test.tsx         |   4 +-
 .../AgentDetailView.skills-procedure.test.tsx      |  59 +++--
 .../app/components/__tests__/AgentsView.test.tsx   |  19 +-
 .../components/__tests__/SkillMultiselect.test.tsx |  81 ++++---
 .../__tests__/useDiscoveredSkillsCache.test.ts     |  16 ++
 .../app/hooks/useDiscoveredSkillsCache.ts          |  13 +-
 .../app/utils/__tests__/agentSkills.test.ts        |  28 +++
 packages/dashboard/app/utils/agentSkills.ts        |  69 +++++-
 packages/i18n/locales/en/app.json                  |  21 +-
 packages/i18n/locales/es/app.json                  |  21 +-
 packages/i18n/locales/fr/app.json                  |  21 +-
 packages/i18n/locales/ko/app.json                  |  21 +-
 packages/i18n/locales/pt-BR/app.json               |  21 +-
 packages/i18n/locales/zh-CN/app.json               |  21 +-
 packages/i18n/locales/zh-TW/app.json               |  21 +-
 25 files changed, 493 insertions(+), 457 deletions(-)

Fusion-Task-Id: FN-9115

Fusion-Task-Lineage: 18229b87-d0dc-41e7-9333-2d12df852486

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 23:14:24 -07:00

73 lines
3.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
const { fetchDiscoveredSkills } = vi.hoisted(() => ({ fetchDiscoveredSkills: vi.fn() }));
vi.mock("../../api", () => ({ fetchDiscoveredSkills }));
import { SkillMultiselect } from "../SkillMultiselect";
const skills = [
{ id: "skill-1", name: "Skill One", relativePath: "skills/one/SKILL.md", enabled: true },
{ id: "skill-2", name: "Skill Two", relativePath: "skills/two/SKILL.md", enabled: false },
{ id: "skill-3", name: "Skill Three", relativePath: "skills/three/SKILL.md", enabled: true },
] as any[];
describe("SkillMultiselect", () => {
beforeEach(() => fetchDiscoveredSkills.mockReset());
it("renders loading then empty state", async () => {
let resolveSkills!: (value: any[]) => void;
fetchDiscoveredSkills.mockReturnValue(new Promise((resolve) => { resolveSkills = resolve; }));
render(<SkillMultiselect value={[]} onChange={vi.fn()} id="skills" />);
expect(screen.getByTestId("skills-loading")).toBeInTheDocument();
resolveSkills([]);
expect(await screen.findByTestId("skills-empty")).toHaveTextContent("No skills discovered");
});
it("selects three visible checkbox rows without a dropdown", async () => {
fetchDiscoveredSkills.mockResolvedValue(skills);
const onChange = vi.fn();
const user = userEvent.setup();
render(<SkillMultiselect value={[]} onChange={onChange} id="skills" />);
await screen.findByTestId("skill-option-skill-1");
await user.click(screen.getByTestId("skill-option-skill-1").querySelector("input")!);
await user.click(screen.getByTestId("skill-option-skill-2").querySelector("input")!);
await user.click(screen.getByTestId("skill-option-skill-3").querySelector("input")!);
expect(screen.queryByRole("combobox")).toBeNull();
expect(onChange).toHaveBeenCalledTimes(3);
expect(screen.getAllByText("Auto-available").length).toBeGreaterThan(0);
expect(screen.getAllByText("Disabled").length).toBeGreaterThan(0);
});
it("filters with retained focus and supports unknown chips", async () => {
fetchDiscoveredSkills.mockResolvedValue(skills);
const user = userEvent.setup();
render(<SkillMultiselect value={["missing", "missing"]} onChange={vi.fn()} id="skills" />);
const filter = await screen.findByTestId("skill-filter");
await user.type(filter, "Three");
expect(filter).toHaveFocus();
expect(filter).toHaveValue("Three");
expect(screen.getByTestId("skill-option-skill-3")).toBeInTheDocument();
expect(screen.queryByTestId("skill-option-skill-1")).toBeNull();
expect(screen.getByTestId("skill-chip-missing")).toHaveTextContent("Not discovered");
});
it("renders error retry, no matches, and disabled populated controls", async () => {
fetchDiscoveredSkills.mockRejectedValueOnce(new Error("offline")).mockResolvedValueOnce(skills);
const user = userEvent.setup();
render(<SkillMultiselect value={["skill-1"]} onChange={vi.fn()} id="skills" disabled />);
expect(await screen.findByTestId("skills-error")).toBeInTheDocument();
expect(screen.getByTestId("skill-chip-skill-1")).toHaveTextContent("Checking availability");
expect(screen.getByTestId("skill-chip-skill-1")).not.toHaveTextContent("Not discovered");
expect(screen.getByRole("button", { name: "Retry" })).toBeDisabled();
// A separate enabled render proves retry and no-match states without hiding controls.
const { unmount } = render(<SkillMultiselect value={[]} onChange={vi.fn()} id="enabled" />);
await user.click(await screen.findAllByRole("button", { name: "Retry" }).then((items) => items[1]!));
const filter = await screen.findByTestId("skill-filter");
await user.type(filter, "absent");
expect(screen.getByTestId("skills-no-matches")).toBeInTheDocument();
unmount();
});
});