foxygit / RPA-Remisser Log in
commits tags

/webui/frontend/src/lib/graph.test.ts · 3.47 KB

raw
import { describe, expect, it } from "vitest";
import type { Flow } from "./api";
import { autoLayout, normalizeFlow, pickStart, remapIds } from "./graph";

const GRAPH: Partial<Flow> = {
  nodes: [
    { id: "a", type: "goto", url: "https://x.se" },
    { id: "b", type: "if", branches: [{ condition: { kind: "var_equals", var: "x", text: "1" } }] },
    { id: "c", type: "screenshot", name: "hit" },
    { id: "d", type: "screenshot", name: "miss" },
  ],
  edges: [
    { id: "e1", source: "a", target: "b", sourceHandle: "out", targetHandle: "in" },
    { id: "e2", source: "b", target: "c", sourceHandle: "case0", targetHandle: "in" },
    { id: "e3", source: "b", target: "d", sourceHandle: "else", targetHandle: "in" },
  ],
};

describe("normalizeFlow", () => {
  it("fills every key and keeps nodes/edges", () => {
    const f = normalizeFlow(GRAPH);
    expect(f.format).toBe("graph");
    expect(f.nodes.map((n) => n.id)).toEqual(["a", "b", "c", "d"]);
    expect(f.edges).toHaveLength(3);
    expect(f.inputs).toEqual([]);
    expect(f.input_sets).toEqual({});
  });

  it("drops edges that reference missing nodes", () => {
    const f = normalizeFlow({ nodes: [{ id: "a", type: "wait" }], edges: [{ id: "e", source: "a", target: "ghost" }] });
    expect(f.edges).toEqual([]);
  });

  it("assigns ids to id-less nodes", () => {
    const f = normalizeFlow({ nodes: [{ type: "wait" }, { type: "wait" }] });
    expect(f.nodes.every((n) => !!n.id)).toBe(true);
    expect(f.nodes[0].id).not.toBe(f.nodes[1].id);
  });

  it("normalizes subflows (id from name, node/edge ids)", () => {
    const f = normalizeFlow({
      nodes: [],
      subflows: [
        { name: "Hämta Bilagor", nodes: [{ type: "wait" }], edges: [] } as never,
        { id: "keep", name: "K", nodes: [], edges: [] },
      ],
    });
    expect(f.subflows).toHaveLength(2);
    expect(f.subflows![0].id).toBe("h-mta-bilagor");
    expect(f.subflows![0].nodes[0].id).toBeTruthy();
    expect(f.subflows![1].id).toBe("keep");
  });

  it("always returns a subflows array", () => {
    expect(normalizeFlow({ nodes: [] }).subflows).toEqual([]);
  });
});

describe("pickStart", () => {
  it("is the node without an incoming edge", () => {
    const f = normalizeFlow(GRAPH);
    expect(pickStart(f.nodes, f.edges)).toBe("a");
  });

  it("falls back to the topmost node when every node has an incoming edge", () => {
    const nodes = [
      { id: "a", type: "wait", position: { x: 0, y: 10 } },
      { id: "b", type: "wait", position: { x: 0, y: 0 } },
    ];
    const edges = [
      { id: "e1", source: "a", target: "b" },
      { id: "e2", source: "b", target: "a" },
    ];
    expect(pickStart(nodes, edges)).toBe("b");
  });
});

describe("autoLayout", () => {
  it("stacks depth downward (y) and spreads siblings sideways (x)", () => {
    const f = normalizeFlow(GRAPH);
    const pos = autoLayout(f.nodes, f.edges);
    expect(pos.a).toEqual({ x: 0, y: 0 });
    expect(pos.b.y).toBeGreaterThan(pos.a.y); // deeper -> further down
    expect(pos.c.y).toBe(pos.d.y); // same depth -> same row
    expect(pos.c.x).not.toBe(pos.d.x); // siblings side by side
  });
});

describe("remapIds", () => {
  it("rewrites node ids and keeps edges consistent", () => {
    const { nodes, edges } = remapIds(
      [{ id: "a", type: "wait" }, { id: "b", type: "wait" }],
      [{ id: "e", source: "a", target: "b" }],
    );
    expect(nodes[0].id).not.toBe("a");
    expect(edges[0].source).toBe(nodes[0].id);
    expect(edges[0].target).toBe(nodes[1].id);
  });
});