Files
fusion/packages/dashboard/app/components/__tests__/ConnectNodeModal.test.tsx
gsxdsm 0304df348f feat(FN-1228): enhance MeshTopology with peer awareness and status legend
- Export MeshTopologyProps interface for external use
- Add peer awareness lines between remote nodes for mesh visualization
- Implement dynamic SVG sizing based on number of remote nodes
- Add status color legend showing online/offline/connecting/error states
- Improve CSS variable handling with fallback values
- Use transform-based positioning for cleaner SVG structure
2026-04-09 12:41:50 -07:00

249 lines
7.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { ConnectNodeModal } from "../ConnectNodeModal";
import type { NodeInfo } from "../../api";
const mockFetch = vi.fn();
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockReset();
});
afterEach(() => {
vi.unstubAllGlobals();
});
function makeNode(overrides: Partial<NodeInfo> = {}): NodeInfo {
return {
id: "node_test",
name: "Test Node",
type: "remote",
status: "online",
maxConcurrent: 2,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
...overrides,
};
}
describe("ConnectNodeModal", () => {
const defaultProps = {
open: true,
onClose: vi.fn(),
onConnected: vi.fn(),
addToast: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it("renders when open", () => {
render(<ConnectNodeModal {...defaultProps} />);
expect(screen.getByLabelText("Connect to Node")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Build Server")).toBeInTheDocument();
expect(screen.getByPlaceholderText("192.168.1.100 or my-server.local")).toBeInTheDocument();
});
it("does not render when closed", () => {
render(<ConnectNodeModal {...defaultProps} open={false} />);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("validates required name field", async () => {
render(<ConnectNodeModal {...defaultProps} />);
// Fill in host but not name
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "192.168.1.100" },
});
// Try to submit
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
expect(await screen.findByText("Node name is required")).toBeInTheDocument();
expect(defaultProps.addToast).not.toHaveBeenCalled();
});
it("validates required host field", () => {
render(<ConnectNodeModal {...defaultProps} />);
// Get the host input directly
const hostInput = screen.getByPlaceholderText("192.168.1.100 or my-server.local");
expect(hostInput).toBeInTheDocument();
// Host should be empty initially
expect(hostInput).toHaveValue("");
});
it("validates port range", async () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "192.168.1.100" },
});
// Port input is the number input (maxConcurrent is also a number input)
const portInput = screen.getAllByRole("spinbutton")[0];
fireEvent.change(portInput, {
target: { value: "99999" },
});
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
expect(await screen.findByText("Port must be between 1 and 65535")).toBeInTheDocument();
});
it("shows URL preview as host and port are filled", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "192.168.1.100" },
});
expect(screen.getByText("http://192.168.1.100:3001")).toBeInTheDocument();
});
it("updates URL preview when port changes", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "my-server.local" },
});
// Port is the first number input
const portInput = screen.getAllByRole("spinbutton")[0];
fireEvent.change(portInput, { target: { value: "8080" } });
expect(screen.getByText("http://my-server.local:8080")).toBeInTheDocument();
});
it("strips protocol from host in URL preview", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "https://my-server.local" },
});
expect(screen.getByText("http://my-server.local:3001")).toBeInTheDocument();
});
it("calls onConnected with registered node on success", async () => {
const node = makeNode({ name: "Test Node", url: "http://192.168.1.100:3001" });
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(node),
});
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "192.168.1.100" },
});
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
await waitFor(() => {
expect(defaultProps.onConnected).toHaveBeenCalledWith(node);
expect(defaultProps.addToast).toHaveBeenCalledWith('Connected to "Test Node"', "success");
expect(defaultProps.onClose).toHaveBeenCalled();
});
});
it("shows error toast on API failure", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
json: () => Promise.resolve({ error: "Invalid node configuration" }),
});
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "invalid-host" },
});
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
await waitFor(() => {
expect(defaultProps.addToast).toHaveBeenCalledWith("Invalid node configuration", "error");
expect(defaultProps.onClose).not.toHaveBeenCalled();
});
});
it("resets form on close", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "192.168.1.100" },
});
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(defaultProps.onClose).toHaveBeenCalled();
});
it("handles Escape key to close", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.keyDown(document, { key: "Escape" });
expect(defaultProps.onClose).toHaveBeenCalled();
});
it("disables connect button when host is empty", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
const connectButton = screen.getByRole("button", { name: "Connect" });
expect(connectButton).toBeDisabled();
});
it("uses custom onSubmit if provided", async () => {
const customOnSubmit = vi.fn().mockResolvedValue(makeNode({ name: "Custom Node" }));
const node = makeNode({ name: "Custom Node" });
render(<ConnectNodeModal {...defaultProps} onSubmit={customOnSubmit} />);
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Custom Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
target: { value: "192.168.1.100" },
});
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
await waitFor(() => {
expect(customOnSubmit).toHaveBeenCalledWith(
expect.objectContaining({
name: "Custom Node",
url: "http://192.168.1.100:3001",
})
);
expect(defaultProps.onConnected).toHaveBeenCalledWith(node);
});
// fetch should not have been called
expect(mockFetch).not.toHaveBeenCalled();
});
});