evOo-local/apps/web/src/pages/KnowledgeSection.test.tsx
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

83 lines
2.9 KiB
TypeScript

// @vitest-environment jsdom
import type { ComponentProps, ReactNode } from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { expect, it, vi } from "vitest";
const api = vi.hoisted(() => ({ list: vi.fn(), create: vi.fn(), get: vi.fn() }));
vi.mock("../lib/rpc", () => ({
rpc: { agentSkills: api, memory: { list: async () => [] } },
}));
vi.mock("../lib/artifact-open", () => ({ downloadArtifactBytes: vi.fn() }));
vi.mock("@lingui/react/macro", () => {
const t = (parts: TemplateStringsArray) => parts.join("");
return { useLingui: () => ({ t }), Trans: ({ children }: { children: ReactNode }) => children };
});
vi.mock("@rakazo/ui-web", () => {
const Container = ({ children }: { children?: ReactNode }) => <div>{children}</div>;
return {
Button: ({ variant: _variant, ...props }: ComponentProps<"button"> & { variant?: string }) => (
<button {...props} />
),
Textarea: (props: ComponentProps<"textarea">) => <textarea {...props} />,
Skeleton: Container,
Tabs: Container,
TabsList: Container,
TabsTrigger: Container,
TabsContent: Container,
};
});
import { KnowledgeSection } from "./KnowledgeSection";
it("disables skill rows while a completed save is refreshing the catalog", async () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
const skill = {
id: "skill-fixture",
name: "greeting",
description: "Greet politely",
content: "Hello",
};
let finishRefresh!: (value: (typeof skill)[]) => void;
const refresh = new Promise<(typeof skill)[]>((resolve) => {
finishRefresh = resolve;
});
api.list.mockResolvedValueOnce([skill]).mockReturnValueOnce(refresh);
api.create.mockResolvedValue(skill);
api.get.mockResolvedValue(skill);
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
const button = (text: string) => {
const found = [...container.querySelectorAll("button")].find((entry) =>
entry.textContent?.includes(text),
);
if (!found) throw new Error(`Missing button: ${text}`);
return found;
};
try {
await act(async () =>
root.render(<KnowledgeSection botId="bot-fixture" onSkillsChange={() => undefined} />),
);
await act(async () => button("New skill").click());
await act(async () => button("Save").click());
expect(api.create).toHaveBeenCalledOnce();
expect(api.list).toHaveBeenCalledTimes(2);
expect(button("greeting").disabled).toBe(true);
await act(async () => button("greeting").click());
expect(api.get).not.toHaveBeenCalled();
await act(async () => finishRefresh([skill]));
expect(button("greeting").disabled).toBe(false);
await act(async () => button("greeting").click());
expect(container.querySelector("textarea")?.value).toBe("Hello");
} finally {
await act(async () => {
finishRefresh([skill]);
root.unmount();
});
container.remove();
vi.unstubAllGlobals();
}
});