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.
83 lines
2.9 KiB
TypeScript
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();
|
|
}
|
|
});
|