Files
2026-07-13 13:39:12 +08:00

215 lines
6.5 KiB
TypeScript

// @vitest-environment jsdom
/**
* UI unit tests for AgentCard — DNS toggle + wizard open.
*
* Note: AgentCard/SetupWizard import @/mitm/types (zod types only, no DB).
* We set testTimeout=30000 to handle the initial transform overhead.
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) =>
React.createElement("a", { href }, children),
}));
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [] }),
} as unknown as Response);
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
// Minimal mock target (matches MitmTarget shape but no heavy imports)
const mockTarget = {
id: "copilot" as const,
name: "GitHub Copilot",
icon: "code",
color: "#10B981",
hosts: ["api.githubcopilot.com"],
port: 443,
endpointPatterns: ["/chat/completions"],
defaultModels: [{ id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" }],
setupTutorial: {
steps: ["Step 1", "Step 2"],
detection: { command: "which copilot", platform: "all" as const },
},
handler: () => Promise.resolve({ default: class {} as never }),
riskNoticeKey: "providers.riskNotice.oauth",
viability: "supported" as const,
};
describe("AgentCard", { timeout: 30000 }, () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders agent name and hosts", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: undefined,
serverRunning: false,
mappings: [],
onDnsToggle: vi.fn(),
onMappingsSave: vi.fn(),
})
);
});
expect(document.body.innerHTML).toContain("GitHub Copilot");
expect(document.body.innerHTML).toContain("api.githubcopilot.com");
}, 30000);
it("expands on click and shows DNS toggle", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: undefined,
serverRunning: true,
mappings: [],
onDnsToggle: vi.fn(),
onMappingsSave: vi.fn(),
})
);
});
const header = container.querySelector("button[aria-expanded]");
expect(header).not.toBeNull();
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(document.body.innerHTML).toContain("startDns");
}, 30000);
it("calls onDnsToggle when DNS button clicked", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
// Simulate that the per-agent RiskNoticeModal (Fix4 M5) has already been
// accepted for this agent — otherwise the DNS click opens the modal first
// and onDnsToggle is only called after the user accepts. We test the
// "already accepted" path here; the modal flow is covered by
// tests/unit/ui/agent-card-risk-modal.test.tsx.
localStorage.setItem("omniroute-agentbridge-risk-dismissed-copilot", "true");
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: {
agent_id: "copilot",
dns_enabled: false,
cert_trusted: false,
setup_completed: false,
last_started_at: null,
last_error: null,
},
serverRunning: true,
mappings: [],
onDnsToggle,
onMappingsSave: vi.fn(),
})
);
});
// Expand card
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Find and click DNS button
const dnsButton = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("startDns")
);
expect(dnsButton).not.toBeNull();
await act(async () => {
dnsButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onDnsToggle).toHaveBeenCalledWith("copilot", true);
}, 30000);
it("opens wizard when setup wizard button clicked", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: undefined,
serverRunning: true,
mappings: [],
onDnsToggle: vi.fn(),
onMappingsSave: vi.fn(),
})
);
});
// Expand card first
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Find setup wizard button
const wizardBtn = Array.from(document.querySelectorAll("button")).find((b) =>
b.textContent?.includes("setupWizard")
);
await act(async () => {
wizardBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
}, 30000);
});