Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a7a86d361 |
@@ -0,0 +1,409 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Tests for ExternalConnectModal component.
|
||||
*
|
||||
* Covers:
|
||||
* - Null info: renders nothing
|
||||
* - Dialog renders with correct title and description
|
||||
* - Default tab: "Universal MCP" when universal_mcp_snippet present, else "Python SDK"
|
||||
* - All 8 tabs render the correct snippet/fields when data is present
|
||||
* - Hidden tabs: runtime-specific tabs absent when platform omits the snippet
|
||||
* - Token stamping: auth_token replaces <paste…> placeholder in snippets
|
||||
* - Copy button: navigator.clipboard.writeText called, "Copied!" shown
|
||||
* - Copy fallback: textarea selected when clipboard access denied
|
||||
* - Close button calls onClose
|
||||
* - Radix Dialog: open prop controls visibility, onOpenChange fires on close
|
||||
* - Accessibility: role=tablist, aria-selected per tab, aria-label on content
|
||||
*/
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent, cleanup, act } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ExternalConnectModal } from "../ExternalConnectModal";
|
||||
|
||||
// ─── Mock clipboard API ────────────────────────────────────────────────────────
|
||||
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
const mockClipboard = { writeText };
|
||||
|
||||
vi.stubGlobal("navigator", {
|
||||
clipboard: mockClipboard,
|
||||
});
|
||||
|
||||
// ─── Mock Radix Dialog (lightweight) ──────────────────────────────────────────
|
||||
|
||||
vi.mock("@radix-ui/react-dialog", () => ({
|
||||
Root: vi.fn(({ children, open, onOpenChange }: { children: React.ReactNode; open: boolean; onOpenChange?: (o: boolean) => void }) => (
|
||||
<>{open ? children : null}</>
|
||||
)),
|
||||
Portal: vi.fn(({ children }: { children: React.ReactNode }) => <>{children}</>),
|
||||
Overlay: vi.fn(({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="dialog-overlay">{children}</div>
|
||||
)),
|
||||
Content: vi.fn(({ children }: { children: React.ReactNode }) => (
|
||||
<div role="dialog" data-testid="dialog-content">{children}</div>
|
||||
)),
|
||||
Title: vi.fn(({ children }: { children: React.ReactNode }) => (
|
||||
<h2>{children}</h2>
|
||||
)),
|
||||
Description: vi.fn(({ children }: { children: React.ReactNode }) => (
|
||||
<p>{children}</p>
|
||||
)),
|
||||
}));
|
||||
|
||||
// ─── Full props factory ────────────────────────────────────────────────────────
|
||||
|
||||
function makeInfo(overrides?: Partial<{
|
||||
universal_mcp_snippet: string;
|
||||
python_snippet: string;
|
||||
claude_code_channel_snippet: string;
|
||||
hermes_channel_snippet: string;
|
||||
codex_snippet: string;
|
||||
openclaw_snippet: string;
|
||||
}>): import("../ExternalConnectModal").ExternalConnectionInfo {
|
||||
return {
|
||||
workspace_id: "ws-test-123",
|
||||
platform_url: "https://platform.example.com",
|
||||
auth_token: "tok_secret_abc",
|
||||
registry_endpoint: "https://platform.example.com/registry/register",
|
||||
heartbeat_endpoint: "https://platform.example.com/registry/heartbeat",
|
||||
curl_register_template:
|
||||
'curl -X POST https://platform.example.com/registry/register \\\n -H "Content-Type: application/json" \\\n -d \'{"workspace_id":"ws-test-123","url":"https://agent.example.com","agent_card":{}}\' \\\n -H "Authorization: Bearer WORKSPACE_AUTH_TOKEN=\\"<paste from create response>\\""',
|
||||
python_snippet:
|
||||
'from molecule_ai import Client\n\nclient = Client(\n platform_url="https://platform.example.com",\n workspace_id="ws-test-123",\n AUTH_TOKEN = "<paste from create response>",\n)\nclient.register(url="https://agent.example.com")',
|
||||
universal_mcp_snippet:
|
||||
'claude mcp add molecule -- \\\n env MOLECULE_WORKSPACE_TOKEN="<paste from create response>"',
|
||||
claude_code_channel_snippet:
|
||||
'MOLECULE_WORKSPACE_TOKENS=<paste auth_token from create response>',
|
||||
hermes_channel_snippet:
|
||||
'MOLECULE_WORKSPACE_TOKEN="<paste from create response>"',
|
||||
codex_snippet:
|
||||
'MOLECULE_WORKSPACE_TOKEN = "<paste from create response>"',
|
||||
openclaw_snippet:
|
||||
'WORKSPACE_TOKEN="<paste from create response>"',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function renderModal(info: import("../ExternalConnectModal").ExternalConnectionInfo | null) {
|
||||
const onClose = vi.fn();
|
||||
const result = render(<ExternalConnectModal info={info} onClose={onClose} />);
|
||||
return { ...result, onClose };
|
||||
}
|
||||
|
||||
function clickTab(name: string) {
|
||||
fireEvent.click(screen.getByRole("tab", { name }));
|
||||
}
|
||||
|
||||
function clickButton(label: string | RegExp) {
|
||||
fireEvent.click(screen.getByRole("button", { name: label }));
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("ExternalConnectModal — null guard", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders nothing when info is null", () => {
|
||||
renderModal(null);
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders nothing when info is null even after timeout", () => {
|
||||
vi.useFakeTimers();
|
||||
renderModal(null);
|
||||
act(() => { vi.runAllTimers(); });
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExternalConnectModal — shell", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders dialog when info is provided", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the title", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByText("Connect your external agent")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the security warning about one-time display", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByText(/only once/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the close button", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByRole("button", { name: /saved it/i })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("close button calls onClose", () => {
|
||||
const { onClose } = renderModal(makeInfo());
|
||||
clickButton(/saved it/i);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExternalConnectModal — default tab selection", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('defaults to "Universal MCP" tab when universal_mcp_snippet is present', () => {
|
||||
renderModal(makeInfo());
|
||||
// The MCP tab should be aria-selected=true
|
||||
expect(screen.getByRole("tab", { name: "Universal MCP" }).getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it('defaults to "Python SDK" tab when universal_mcp_snippet is absent', () => {
|
||||
renderModal(makeInfo({ universal_mcp_snippet: undefined }));
|
||||
expect(screen.getByRole("tab", { name: "Python SDK" }).getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it('"Universal MCP" tab is absent when universal_mcp_snippet is undefined', () => {
|
||||
renderModal(makeInfo({ universal_mcp_snippet: undefined }));
|
||||
expect(screen.queryByRole("tab", { name: "Universal MCP" })).toBeNull();
|
||||
});
|
||||
|
||||
it('"Claude Code" tab is absent when claude_code_channel_snippet is undefined', () => {
|
||||
renderModal(makeInfo({ claude_code_channel_snippet: undefined }));
|
||||
expect(screen.queryByRole("tab", { name: "Claude Code" })).toBeNull();
|
||||
});
|
||||
|
||||
it('"Hermes" tab is absent when hermes_channel_snippet is undefined', () => {
|
||||
renderModal(makeInfo({ hermes_channel_snippet: undefined }));
|
||||
expect(screen.queryByRole("tab", { name: "Hermes" })).toBeNull();
|
||||
});
|
||||
|
||||
it('"Codex" tab is absent when codex_snippet is undefined', () => {
|
||||
renderModal(makeInfo({ codex_snippet: undefined }));
|
||||
expect(screen.queryByRole("tab", { name: "Codex" })).toBeNull();
|
||||
});
|
||||
|
||||
it('"OpenClaw" tab is absent when openclaw_snippet is undefined', () => {
|
||||
renderModal(makeInfo({ openclaw_snippet: undefined }));
|
||||
expect(screen.queryByRole("tab", { name: "OpenClaw" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExternalConnectModal — tab switching", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("clicking Python tab switches aria-selected", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Python SDK");
|
||||
expect(screen.getByRole("tab", { name: "Python SDK" }).getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it("clicking curl tab shows curl snippet", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("curl");
|
||||
expect(screen.getByText(/curl -X POST/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking Fields tab shows all field rows", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Fields");
|
||||
expect(screen.getByText("workspace_id")).toBeTruthy();
|
||||
expect(screen.getByText("ws-test-123")).toBeTruthy();
|
||||
expect(screen.getByText("auth_token")).toBeTruthy();
|
||||
expect(screen.getByText("platform_url")).toBeTruthy();
|
||||
expect(screen.getByText("registry_endpoint")).toBeTruthy();
|
||||
expect(screen.getByText("heartbeat_endpoint")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking Universal MCP tab shows the snippet with token stamped", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Universal MCP");
|
||||
// The token should be stamped, not the placeholder
|
||||
expect(screen.getByText(/tok_secret_abc/i)).toBeTruthy();
|
||||
expect(screen.queryByText(/<paste.*>/i)).toBeNull();
|
||||
});
|
||||
|
||||
it("clicking Python SDK tab shows snippet with token stamped", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Python SDK");
|
||||
expect(screen.getByText(/tok_secret_abc/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking Hermes tab shows snippet with token stamped", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Hermes");
|
||||
expect(screen.getByText(/tok_secret_abc/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking Codex tab shows snippet with token stamped", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Codex");
|
||||
expect(screen.getByText(/tok_secret_abc/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking OpenClaw tab shows snippet with token stamped", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("OpenClaw");
|
||||
expect(screen.getByText(/tok_secret_abc/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking Claude Code tab shows channel snippet with token stamped", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Claude Code");
|
||||
expect(screen.getByText(/tok_secret_abc/i)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExternalConnectModal — copy button", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("Copy button calls navigator.clipboard.writeText", async () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("curl");
|
||||
const copyBtn = screen.getByRole("button", { name: "Copy" });
|
||||
await act(async () => { fireEvent.click(copyBtn); });
|
||||
expect(mockClipboard.writeText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("Copy button shows 'Copied!' after click", async () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("curl");
|
||||
const copyBtn = screen.getByRole("button", { name: "Copy" });
|
||||
await act(async () => { fireEvent.click(copyBtn); });
|
||||
expect(screen.getByRole("button", { name: "Copied!" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Copied! label clears after 1.5s (clipboard auto-reset)", async () => {
|
||||
vi.useFakeTimers();
|
||||
renderModal(makeInfo());
|
||||
clickTab("curl");
|
||||
const copyBtn = screen.getByRole("button", { name: "Copy" });
|
||||
await act(async () => { fireEvent.click(copyBtn); });
|
||||
expect(screen.getByRole("button", { name: "Copied!" })).toBeTruthy();
|
||||
act(() => { vi.runAllTimers(); });
|
||||
// After timeout, button reverts to "Copy"
|
||||
expect(screen.getByRole("button", { name: "Copy" })).toBeTruthy();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("Copied! label resets on second copy click", async () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("curl");
|
||||
const copyBtn = screen.getByRole("button", { name: "Copy" });
|
||||
await act(async () => { fireEvent.click(copyBtn); });
|
||||
expect(screen.getByRole("button", { name: "Copied!" })).toBeTruthy();
|
||||
await act(async () => { fireEvent.click(copyBtn); });
|
||||
// Second click resets to "Copy" (auto-clear fires, then new click sets again)
|
||||
// The auto-clear timeout fires and resets, then the new click sets it
|
||||
// In practice: after 1.5s it reverts to Copy; immediate second click resets immediately
|
||||
expect(mockClipboard.writeText).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("clipboard failure: textarea fallback selected without throwing", async () => {
|
||||
mockClipboard.writeText.mockRejectedValueOnce(new Error("clipboard denied"));
|
||||
renderModal(makeInfo());
|
||||
clickTab("Fields");
|
||||
// The fields tab has a Copy button per row
|
||||
const copyBtns = screen.getAllByRole("button", { name: "Copy" });
|
||||
// Trigger copy on the auth_token field
|
||||
await act(async () => { fireEvent.click(copyBtns[copyBtns.length - 1]); });
|
||||
// Should not throw — error is caught
|
||||
expect(mockClipboard.writeText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExternalConnectModal — Fields tab", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows workspace_id value", () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Fields");
|
||||
expect(screen.getByText("ws-test-123")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows platform_url value", () => {
|
||||
renderModal(makeInfo({ platform_url: "https://custom.example.com" }));
|
||||
clickTab("Fields");
|
||||
expect(screen.getByText("https://custom.example.com")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows masked auth_token (full value visible)", () => {
|
||||
// Note: the modal shows the full token for operator to copy.
|
||||
// The platform does not mask it (by design — operator just saw it once).
|
||||
renderModal(makeInfo());
|
||||
clickTab("Fields");
|
||||
expect(screen.getByText("tok_secret_abc")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Copy button on Fields rows calls copy with correct value", async () => {
|
||||
renderModal(makeInfo());
|
||||
clickTab("Fields");
|
||||
const copyBtns = screen.getAllByRole("button", { name: "Copy" });
|
||||
// Click the first copy button (workspace_id row)
|
||||
await act(async () => { fireEvent.click(copyBtns[0]); });
|
||||
expect(mockClipboard.writeText).toHaveBeenCalledWith("ws-test-123");
|
||||
});
|
||||
|
||||
it("shows '(missing)' for empty string field values", () => {
|
||||
renderModal(makeInfo({ platform_url: "" }));
|
||||
clickTab("Fields");
|
||||
expect(screen.getByText("(missing)")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ExternalConnectModal — accessibility", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("tablist has role=tablist", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByRole("tablist")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("tablist has aria-label", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByRole("tablist").getAttribute("aria-label")).toBe("Connection snippet format");
|
||||
});
|
||||
|
||||
it("each visible tab has role=tab", () => {
|
||||
renderModal(makeInfo());
|
||||
const tabs = screen.getAllByRole("tab");
|
||||
expect(tabs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("active tab has aria-selected=true", () => {
|
||||
renderModal(makeInfo());
|
||||
// Default tab is Universal MCP
|
||||
expect(screen.getByRole("tab", { name: "Universal MCP" }).getAttribute("aria-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it("inactive tab has aria-selected=false", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByRole("tab", { name: "Python SDK" }).getAttribute("aria-selected")).toBe("false");
|
||||
});
|
||||
|
||||
it("dialog has role=dialog", () => {
|
||||
renderModal(makeInfo());
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user