import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { type NodeProps, Position } from "@xyflow/react"; import { describe, expect, it, vi } from "vitest"; import { ResourceNode } from "@/components/attack-paths/graph/nodes/resource-node"; import type { GraphNode } from "@/types/attack-paths"; const hiddenHandlesMock = vi.hoisted(() => vi.fn(() => null)); vi.mock("@/components/attack-paths/graph/nodes/hidden-handles", () => ({ HiddenHandles: hiddenHandlesMock, })); const buildGraphNode = (label: string, name: string): GraphNode => ({ id: `${label}-${name}`, labels: [label], properties: { id: `${label}-${name}`, name }, }); const buildNodeProps = (graphNode: GraphNode): NodeProps => ({ id: graphNode.id, type: "resource", data: { graphNode }, selected: false, dragging: false, zIndex: 0, isConnectable: false, positionAbsoluteX: 0, positionAbsoluteY: 0, }) as unknown as NodeProps; describe("ResourceNode", () => { it("positions graph handles for horizontal left-to-right edges", () => { // Given const props = buildNodeProps(buildGraphNode("S3Bucket", "logs")); // When render(); // Then expect(hiddenHandlesMock).toHaveBeenCalledWith( expect.objectContaining({ sourcePosition: Position.Right, sourceStyle: { left: 90, top: 26 }, targetPosition: Position.Left, targetStyle: { left: 46, top: 26 }, }), undefined, ); }); describe("node visual icons", () => { it("should render the S3 bucket icon with the resource label", () => { // Given const props = buildNodeProps(buildGraphNode("S3Bucket", "logs")); // When render(); // Then expect( screen.getByTestId("attack-path-node-icon-s3-bucket"), ).toHaveAccessibleName("S3 Bucket icon"); expect(screen.getByText("logs")).toBeInTheDocument(); expect(screen.getByText("S3 Bucket")).toBeInTheDocument(); }); it("should render a distinct VPC icon with the resource label", () => { // Given const props = buildNodeProps(buildGraphNode("VPC", "main-vpc")); // When render(); // Then expect( screen.getByTestId("attack-path-node-icon-vpc"), ).toHaveAccessibleName("VPC icon"); expect( screen.queryByTestId("attack-path-node-icon-s3-bucket"), ).not.toBeInTheDocument(); expect(screen.getByText("main-vpc")).toBeInTheDocument(); expect(screen.getByText("VPC")).toBeInTheDocument(); }); it("should show up to four readable lines for long resource names", () => { // Given const props = buildNodeProps( buildGraphNode("AWSRole", "AWSReservedSSO_AdministratorAccessExtra"), ); // When const { container } = render(); // Then expect(screen.getByText("AWSReservedSSO_A")).toBeInTheDocument(); expect(screen.getByText("dministratorAcce")).toBeInTheDocument(); expect(screen.getByText("ssExtra")).toBeInTheDocument(); expect(screen.getByText("AWS Role")).toBeInTheDocument(); expect(container.querySelector("title")).toBeNull(); }); it("should expose the full resource name as an immediate tooltip when truncated", async () => { // Given const name = "arn:aws:iam::998057895221:role/OrganizationAccountAccessRole/integration"; const props = buildNodeProps(buildGraphNode("AWSRole", name)); // When render(); // Then expect(screen.getByText("arn:aws:iam::998")).toBeInTheDocument(); expect(screen.getByText("057895221:role/O")).toBeInTheDocument(); expect(screen.getByText("ntAccessRole/in…")).toBeInTheDocument(); await userEvent.hover(screen.getByTestId("attack-path-resource-node")); expect(await screen.findAllByText(name)).not.toHaveLength(0); }); }); });