nexus/ui/src/components/settings/SkillsSection.test.tsx
Nexus Dev 6aeadeeb11 feat(nexus): add Telegram, About, and DangerZone settings cards (phase 13)
TelegramSection wraps /telegram/token (masked password Input) and
/telegram/status so the bot token can be set or replaced inline.
Values are never logged.

AboutSection renders app name, version, fork lineage, and MIT license
with an outbound link.

DangerZoneSection marks the reset-workspace and delete-all-
conversations actions from spec §8.1 as disabled placeholders — their
server endpoints are not yet wired, and fabricating fake ones would
break the "preserve existing functionality" rule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:25:26 +00:00

104 lines
3.6 KiB
TypeScript

// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MemoryRouter } from "@/lib/router";
vi.mock("@/api/skillRegistry", () => ({
skillRegistryApi: {
list: vi.fn(async () => [
{ id: "a/one", name: "One", description: null, sourceId: "a", category: null, activeVersionId: "v1", removedAt: null, averageRating: null, ratingCount: null, taskCount: null, avgCostUsd: null, lastUsedAt: null },
{ id: "a/two", name: "Two", description: null, sourceId: "a", category: null, activeVersionId: "v1", removedAt: null, averageRating: null, ratingCount: null, taskCount: null, avgCostUsd: null, lastUsedAt: null },
{ id: "a/removed", name: "R", description: null, sourceId: "a", category: null, activeVersionId: null, removedAt: 123, averageRating: null, ratingCount: null, taskCount: null, avgCostUsd: null, lastUsedAt: null },
]),
},
}));
vi.mock("@/api/skillGroups", () => ({
skillGroupsApi: {
listGroups: vi.fn(async () => [{ id: "g1" }, { id: "g2" }]),
},
}));
vi.mock("@/context/CompanyContext", () => ({
useCompany: () => ({
companies: [],
selectedCompanyId: "c1",
selectedCompany: { id: "c1", name: "Test", issuePrefix: "NEX" },
selectionSource: "manual" as const,
loading: false,
error: null,
setSelectedCompanyId: () => {},
reloadCompanies: async () => {},
createCompany: async () => {
throw new Error("not implemented");
},
}),
}));
import { SkillsSection } from "./SkillsSection";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
describe("SkillsSection", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot> | null = null;
let queryClient: QueryClient;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = null;
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
});
afterEach(() => {
if (root) {
act(() => {
root!.unmount();
});
root = null;
}
if (container.parentNode) container.remove();
queryClient.clear();
});
async function renderAndFlush() {
root = createRoot(container);
act(() => {
root!.render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={["/instance/settings/general"]}>
<SkillsSection />
</MemoryRouter>
</QueryClientProvider>,
);
});
for (let i = 0; i < 20; i++) {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
}
it("renders the Skills section and counts only non-removed skills", async () => {
await renderAndFlush();
expect(container.querySelector("h2")?.textContent).toBe("Skills");
expect(container.textContent).toContain("Installed skills");
// Exactly 2 non-removed skills from the mock
const counts = Array.from(container.querySelectorAll("span")).filter(
(s) => s.textContent === "2",
);
expect(counts.length).toBeGreaterThan(0);
});
it("links Open Skill Aggregator to the company-prefixed skills route", async () => {
await renderAndFlush();
const openLink = container.querySelector("a[aria-label='Open Skill Aggregator']");
expect(openLink).not.toBeNull();
expect(openLink?.getAttribute("href")).toBe("/NEX/skills");
});
});