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>
104 lines
3.6 KiB
TypeScript
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");
|
|
});
|
|
});
|