Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

62 changes: 0 additions & 62 deletions src/loadpath/static/assets/index-Cj5VBWfS.js

This file was deleted.

62 changes: 62 additions & 0 deletions src/loadpath/static/assets/index-DuTMxUNT.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/loadpath/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="./assets/index-Cj5VBWfS.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-eMEYJf2U.css">
<script type="module" crossorigin src="./assets/index-DuTMxUNT.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CCz64vFJ.css">
</head>
<body>
<div id="root"></div>
Expand Down
25 changes: 25 additions & 0 deletions tests/e2e/test_ui_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ def test_ui_index_review_graph_copy_and_workspace(live_app, browser_page):
layout = page.get_by_test_id("graph-layout")
layout.wait_for()
assert layout.input_value() == "layers"
labels = layout.locator("option").all_inner_texts()
for expected in (
"Architecture layers",
"Edge flow",
"Spanning tree",
"Radial",
"Concentric layers",
"Circle",
"Type clusters",
"Compact grid",
"Force directed",
):
assert expected in labels, labels
before = invoice.get_attribute("style")
layout.select_option("radial")
page.wait_for_function(
Expand All @@ -126,6 +139,18 @@ def test_ui_index_review_graph_copy_and_workspace(live_app, browser_page):
timeout=10_000,
)
assert layout.input_value() == "radial"
before_force = invoice.get_attribute("style")
layout.select_option("force")
page.wait_for_function(
"""before => {
const n = [...document.querySelectorAll('.react-flow__node')]
.find(el => (el.textContent || '').includes('InvoicePage'));
return Boolean(n && n.getAttribute('style') !== before);
}""",
arg=before_force,
timeout=10_000,
)
assert layout.input_value() == "force"

page.get_by_test_id("tab-graph").click()
page.get_by_test_id("graph-full").wait_for()
Expand Down
2 changes: 1 addition & 1 deletion ui/src/ImpactGraph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("toReactFlowElements", () => {
expect(rfEdges[0].targetHandle).toBe("tgt-w");
});

it("uses bezier edges and different positions for radial layout", () => {
it("uses bezier edges and different positions for freeform layouts", () => {
const layered = toReactFlowElements(nodes, edges);
const radial = toReactFlowElements(nodes, edges, null, { layout: "radial" });
expect(radial.rfEdges[0]?.type).toBe("default");
Expand Down
80 changes: 72 additions & 8 deletions ui/src/graphView.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it } from "vitest";
import {
GRAPH_LAYOUTS,
LARGE_GRAPH,
LAYER_LABELS,
defaultDetail,
Expand Down Expand Up @@ -147,15 +148,16 @@ describe("layoutGraph", () => {
expect(pos.get("t")!.x).toBeGreaterThan(pos.get("m")!.x);
});

it("radial and grid produce finite coordinates for every node", () => {
for (const layout of ["radial", "grid"] as const) {
const pos = layoutGraph(nodes, edges, layout);
it("every listed algorithm places every node at finite coordinates", () => {
for (const { id } of GRAPH_LAYOUTS) {
const pos = layoutGraph(nodes, edges, id);
expect(pos.size).toBe(nodes.length);
for (const n of nodes) {
const p = pos.get(n.id)!;
expect(Number.isFinite(p.x)).toBe(true);
expect(Number.isFinite(p.y)).toBe(true);
}
expect(layoutGraph([], [], id).size).toBe(0);
}
});

Expand All @@ -176,11 +178,73 @@ describe("layoutGraph", () => {
expect(span).toBeLessThanOrEqual(GRAPH_NODE_WIDTH + GRAPH_COL_GAP);
});

it("treats flow as columns and radial/grid as freeform", () => {
it("tree stacks descendants below their parent", () => {
const chain = [node("s", "django.view", "S"), node("m", "django.serializer", "M"), node("t", "react.page", "T")];
const pos = layoutGraph(chain, [edge("s", "m"), edge("m", "t")], "tree");
expect(pos.get("m")!.y).toBeGreaterThan(pos.get("s")!.y);
expect(pos.get("t")!.y).toBeGreaterThan(pos.get("m")!.y);
});

it("circle keeps nodes on one ring", () => {
const ring = [
node("a", "django.view", "A"),
node("b", "django.serializer", "B"),
node("c", "django.model", "C"),
node("d", "react.page", "D"),
];
const pos = layoutGraph(ring, [], "circle");
const radii = ring.map((n) => Math.hypot(pos.get(n.id)!.x, pos.get(n.id)!.y));
const mean = radii.reduce((s, r) => s + r, 0) / radii.length;
for (const r of radii) expect(Math.abs(r - mean)).toBeLessThan(1e-6);
expect(mean).toBeGreaterThan(0);
});

it("concentric puts later architecture layers on a larger ring", () => {
const inner = node("v", "django.view", "View");
const outer = node("p", "react.page", "Page");
const pos = layoutGraph([inner, outer], [], "concentric");
const rInner = Math.hypot(pos.get("v")!.x, pos.get("v")!.y);
const rOuter = Math.hypot(pos.get("p")!.x, pos.get("p")!.y);
expect(rOuter).toBeGreaterThan(rInner);
});

it("clusters keep same-type nodes closer than other types", () => {
const group = [
node("a1", "django.view", "A1"),
node("a2", "django.view", "A2"),
node("b1", "react.page", "B1"),
node("b2", "react.page", "B2"),
];
const pos = layoutGraph(group, [], "clusters");
const dist = (a: string, b: string) => {
const pa = pos.get(a)!;
const pb = pos.get(b)!;
return Math.hypot(pa.x - pb.x, pa.y - pb.y);
};
expect(dist("a1", "a2")).toBeLessThan(dist("a1", "b1"));
expect(dist("b1", "b2")).toBeLessThan(dist("b1", "a1"));
});

it("force layout is deterministic and pulls a linked pair together", () => {
const trio = [node("a", "django.view", "A"), node("b", "django.serializer", "B"), node("c", "react.page", "C")];
const linked = [edge("a", "b")];
const first = layoutGraph(trio, linked, "force");
const second = layoutGraph(trio, linked, "force");
expect(first).toEqual(second);
const dist = (map: typeof first, a: string, b: string) => {
const pa = map.get(a)!;
const pb = map.get(b)!;
return Math.hypot(pa.x - pb.x, pa.y - pb.y);
};
expect(dist(first, "a", "b")).toBeLessThan(dist(first, "a", "c"));
});

it("treats only architecture and flow layouts as columns", () => {
expect(layoutUsesColumns("layers")).toBe(true);
expect(layoutUsesColumns("flow")).toBe(true);
expect(layoutUsesColumns("radial")).toBe(false);
expect(layoutUsesColumns("grid")).toBe(false);
for (const id of ["tree", "radial", "concentric", "circle", "clusters", "grid", "force"] as const) {
expect(layoutUsesColumns(id)).toBe(false);
}
});
});

Expand All @@ -195,7 +259,7 @@ describe("graph layout preference", () => {
});

it("round-trips a valid layout", () => {
writeGraphLayout("radial");
expect(readGraphLayout()).toBe("radial");
writeGraphLayout("force");
expect(readGraphLayout()).toBe("force");
});
});
Loading
Loading