feat(FN-1838): merge fusion/fn-1838
This commit is contained in:
@@ -188,4 +188,178 @@ describe("NodeCard", () => {
|
||||
// Remote node should show only 1 project (explicitly assigned only)
|
||||
expect(screen.getByText("1")).toBeDefined();
|
||||
});
|
||||
|
||||
describe("multi-node scenarios", () => {
|
||||
it("renders remote node with long URL", () => {
|
||||
const longUrl = "https://this-is-a-very-long-hostname.example.com/some/very/long/path/to/resource";
|
||||
const node = makeNode({
|
||||
id: "node-long-url",
|
||||
name: "Long URL Node",
|
||||
type: "remote",
|
||||
url: longUrl,
|
||||
status: "online",
|
||||
});
|
||||
|
||||
render(
|
||||
<NodeCard
|
||||
node={node}
|
||||
projects={[]}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Node name should be visible
|
||||
expect(screen.getByText("Long URL Node")).toBeDefined();
|
||||
|
||||
// URL should be visible (component may truncate it)
|
||||
expect(screen.getByText(/this-is-a-very-long-hostname/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders node with connecting status", () => {
|
||||
const node = makeNode({
|
||||
id: "node-connecting",
|
||||
name: "Connecting Node",
|
||||
type: "remote",
|
||||
url: "https://connecting.example.com",
|
||||
status: "connecting",
|
||||
});
|
||||
|
||||
render(
|
||||
<NodeCard
|
||||
node={node}
|
||||
projects={[]}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Connecting Node")).toBeDefined();
|
||||
expect(screen.getByText("Connecting")).toBeDefined();
|
||||
expect(screen.getByText("Remote")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders node with error status", () => {
|
||||
const node = makeNode({
|
||||
id: "node-error",
|
||||
name: "Error Node",
|
||||
type: "remote",
|
||||
url: "https://error.example.com",
|
||||
status: "error",
|
||||
});
|
||||
|
||||
render(
|
||||
<NodeCard
|
||||
node={node}
|
||||
projects={[]}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Error Node")).toBeDefined();
|
||||
expect(screen.getByText("Error")).toBeDefined();
|
||||
expect(screen.getByText("Remote")).toBeDefined();
|
||||
});
|
||||
|
||||
it("remove button arms on first click, removes on second click", () => {
|
||||
const onRemove = vi.fn();
|
||||
const node = makeNode({ id: "node-remove-test", name: "Remove Test Node" });
|
||||
|
||||
render(
|
||||
<NodeCard
|
||||
node={node}
|
||||
projects={[]}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
);
|
||||
|
||||
// First click arms the button (shows confirm)
|
||||
const removeButton = screen.getByLabelText("Remove node");
|
||||
fireEvent.click(removeButton);
|
||||
|
||||
// Should show confirm text
|
||||
expect(screen.getByText("Confirm")).toBeDefined();
|
||||
expect(onRemove).not.toHaveBeenCalled();
|
||||
|
||||
// Second click removes
|
||||
fireEvent.click(screen.getByLabelText("Confirm remove node"));
|
||||
expect(onRemove).toHaveBeenCalledWith(node.id);
|
||||
});
|
||||
|
||||
it("disarms remove on clicking the armed button again", () => {
|
||||
const onRemove = vi.fn();
|
||||
const node = makeNode({ id: "node-disarm", name: "Disarm Test Node" });
|
||||
|
||||
render(
|
||||
<NodeCard
|
||||
node={node}
|
||||
projects={[]}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
);
|
||||
|
||||
// First click arms the button
|
||||
const removeButton = screen.getByLabelText("Remove node");
|
||||
fireEvent.click(removeButton);
|
||||
|
||||
// Should show confirm text
|
||||
expect(screen.getByText("Confirm")).toBeDefined();
|
||||
|
||||
// Click the armed button again to disarm (should not trigger remove)
|
||||
const armedButton = screen.getByLabelText("Confirm remove node");
|
||||
// Click the button again (third click) to disarm
|
||||
fireEvent.click(armedButton);
|
||||
|
||||
// Should not call remove (it was disarmed, not confirmed)
|
||||
// The button should now be disarmed back to "Remove" state
|
||||
expect(screen.getByText("Remove")).toBeDefined();
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders sample seed nodes correctly", () => {
|
||||
// Test the actual seed data nodes
|
||||
const seedNodes = [
|
||||
makeNode({ id: "node-staging-seed", name: "Staging Server X", type: "remote", url: "https://staging.runfusion.ai", status: "online", maxConcurrent: 4 }),
|
||||
makeNode({ id: "node-gpu-seed", name: "GPU Cluster Y", type: "remote", url: "https://gpu.runfusion.ai", status: "offline", maxConcurrent: 16 }),
|
||||
makeNode({ id: "node-dev-seed", name: "Dev Box Z", type: "remote", url: "http://192.168.1.100:4040", status: "error", maxConcurrent: 2 }),
|
||||
];
|
||||
|
||||
for (const node of seedNodes) {
|
||||
render(
|
||||
<NodeCard
|
||||
node={node}
|
||||
projects={[]}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify node is rendered with correct data
|
||||
expect(screen.getByText(node.name, { exact: true })).toBeDefined();
|
||||
|
||||
// Verify correct type badge
|
||||
const typeBadge = document.querySelector(".node-card__type-badge");
|
||||
expect(typeBadge?.textContent).toBe("Remote");
|
||||
|
||||
// Verify correct status
|
||||
const statusClass = `.node-card__status--${node.status}`;
|
||||
const statusElement = document.querySelector(statusClass);
|
||||
expect(statusElement).toBeInTheDocument();
|
||||
|
||||
// Clear between renders
|
||||
if (node !== seedNodes[seedNodes.length - 1]) {
|
||||
render(null as unknown as JSX.Element);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,4 +190,83 @@ describe("NodesView", () => {
|
||||
fireEvent.click(closeButton);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe("multi-node dashboard scenarios", () => {
|
||||
it("renders 6 sample nodes with correct stats and mesh topology", () => {
|
||||
// Mock 6 nodes matching the seed data: 1 local + 5 remote
|
||||
const sampleNodes = [
|
||||
makeNode({ id: "node-local", name: "local", type: "local", status: "online", maxConcurrent: 4 }),
|
||||
makeNode({ id: "node-staging", name: "Staging Server", type: "remote", url: "https://staging.runfusion.ai", status: "online", maxConcurrent: 4 }),
|
||||
makeNode({ id: "node-build", name: "Build Machine", type: "remote", url: "https://build.runfusion.ai", status: "online", maxConcurrent: 8 }),
|
||||
makeNode({ id: "node-gpu", name: "GPU Cluster", type: "remote", url: "https://gpu.runfusion.ai", status: "offline", maxConcurrent: 16 }),
|
||||
makeNode({ id: "node-dev", name: "Dev Box (John)", type: "remote", url: "http://192.168.1.100:4040", status: "error", maxConcurrent: 2 }),
|
||||
makeNode({ id: "node-qa", name: "QA Environment", type: "remote", url: "https://qa.runfusion.ai", status: "connecting", maxConcurrent: 4 }),
|
||||
];
|
||||
|
||||
mockUseNodes.mockReturnValue(makeUseNodesResult({ nodes: sampleNodes }));
|
||||
|
||||
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
|
||||
|
||||
// Check stats bar shows correct counts
|
||||
expect(screen.getByTestId("nodes-stat-total").textContent).toContain("6");
|
||||
expect(screen.getByTestId("nodes-stat-online").textContent).toContain("3"); // local + 2 remote
|
||||
expect(screen.getByTestId("nodes-stat-offline").textContent).toContain("2"); // error + offline
|
||||
expect(screen.getByTestId("nodes-stat-remote").textContent).toContain("5");
|
||||
|
||||
// Check 6 node cards are rendered
|
||||
const nodeCards = document.querySelectorAll(".node-card");
|
||||
expect(nodeCards).toHaveLength(6);
|
||||
|
||||
// Check mesh topology is visible
|
||||
const svg = document.querySelector(".mesh-topology__svg");
|
||||
expect(svg).toBeInTheDocument();
|
||||
|
||||
// Check header shows correct count
|
||||
expect(screen.getByText("6 registered")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders all node names and statuses correctly", () => {
|
||||
const sampleNodes = [
|
||||
makeNode({ id: "node-alpha-xyz", name: "Alpha Node Xyz", status: "online", type: "local" }),
|
||||
makeNode({ id: "node-beta-uvw", name: "Beta Node Uvw", status: "offline", type: "remote", url: "https://beta.node" }),
|
||||
makeNode({ id: "node-gamma-rst", name: "Gamma Node Rst", status: "error", type: "remote", url: "https://gamma.node" }),
|
||||
makeNode({ id: "node-delta-opq", name: "Delta Node Opq", status: "connecting", type: "remote", url: "https://delta.node" }),
|
||||
];
|
||||
|
||||
mockUseNodes.mockReturnValue(makeUseNodesResult({ nodes: sampleNodes }));
|
||||
|
||||
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
|
||||
|
||||
// Verify all node names are displayed (unique names to avoid collisions)
|
||||
expect(screen.getByText("Alpha Node Xyz", { exact: true })).toBeDefined();
|
||||
expect(screen.getByText("Beta Node Uvw", { exact: true })).toBeDefined();
|
||||
expect(screen.getByText("Gamma Node Rst", { exact: true })).toBeDefined();
|
||||
expect(screen.getByText("Delta Node Opq", { exact: true })).toBeDefined();
|
||||
|
||||
// Verify statuses are displayed (check existence)
|
||||
const onlineElements = document.querySelectorAll(".node-card__status--online");
|
||||
const offlineElements = document.querySelectorAll(".node-card__status--offline");
|
||||
const errorElements = document.querySelectorAll(".node-card__status--error");
|
||||
const connectingElements = document.querySelectorAll(".node-card__status--connecting");
|
||||
|
||||
expect(onlineElements.length).toBe(1);
|
||||
expect(offlineElements.length).toBe(1);
|
||||
expect(errorElements.length).toBe(1);
|
||||
expect(connectingElements.length).toBe(1);
|
||||
});
|
||||
|
||||
it("shows empty state mesh topology indicator when only local node exists", () => {
|
||||
mockUseNodes.mockReturnValue(makeUseNodesResult({
|
||||
nodes: [makeNode({ id: "node-local", name: "local", type: "local", status: "online" })],
|
||||
}));
|
||||
|
||||
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
|
||||
|
||||
// Stats should show only local
|
||||
expect(screen.getByTestId("nodes-stat-total").textContent).toContain("1");
|
||||
expect(screen.getByTestId("nodes-stat-online").textContent).toContain("1");
|
||||
expect(screen.getByTestId("nodes-stat-offline").textContent).toContain("0");
|
||||
expect(screen.getByTestId("nodes-stat-remote").textContent).toContain("0");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user