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
97 changes: 78 additions & 19 deletions cmd/mdl/svg_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,30 @@ import (
"github.com/jaschaephraim/lrserver"
)

type nodeOverflow struct {
Name string `json:"name"`
Top float64 `json:"top"`
Right float64 `json:"right"`
Bottom float64 `json:"bottom"`
Left float64 `json:"left"`
}
type (
nodeOverflow struct {
Name string `json:"name"`
Top float64 `json:"top"`
Right float64 `json:"right"`
Bottom float64 `json:"bottom"`
Left float64 `json:"left"`
}

type verticalEdgeLabelOverlap struct {
Source string `json:"source"`
Destination string `json:"destination"`
LineX float64 `json:"lineX"`
LabelLeft float64 `json:"labelLeft"`
LabelRight float64 `json:"labelRight"`
}
verticalEdgeLabelOverlap struct {
Source string `json:"source"`
Destination string `json:"destination"`
LineX float64 `json:"lineX"`
LabelLeft float64 `json:"labelLeft"`
LabelRight float64 `json:"labelRight"`
}

exportDimensions struct {
Width float64 `json:"width"`
Height float64 `json:"height"`
ExpectedWidth float64 `json:"expectedWidth"`
ExpectedHeight float64 `json:"expectedHeight"`
}
)

func hasChrome() bool {
if os.Getenv("CHROME_BIN") != "" {
Expand Down Expand Up @@ -158,9 +167,16 @@ func TestManualEditAfterAutoLayout(t *testing.T) {
await graph.autoLayout();
const node = graph.nodes()[0];
graph.moveNode(node, node.x + 20, node.y, true);
return graph.exportSVG().length;
const markup = graph.exportSVG();
const exported = new DOMParser().parseFromString(markup, "image/svg+xml").documentElement;
return {
width: Number(exported.getAttribute("width")),
height: Number(exported.getAttribute("height")),
expectedWidth: graph.validatedLayout.bounds.width + 100,
expectedHeight: graph.validatedLayout.bounds.height + 100,
};
})()`
var svgLength int
var dimensions exportDimensions
if err := chromedp.Run(
testContext,
chromedp.Navigate(pageURL),
Expand All @@ -170,16 +186,22 @@ func TestManualEditAfterAutoLayout(t *testing.T) {
),
chromedp.Evaluate(
script,
&svgLength,
&dimensions,
func(params *cdruntime.EvaluateParams) *cdruntime.EvaluateParams {
return params.WithAwaitPromise(true)
},
),
); err != nil {
t.Fatalf("auto layout, edit, and export: %v", err)
}
if svgLength == 0 {
t.Fatal("manual export is empty")
if dimensions.Width != dimensions.ExpectedWidth || dimensions.Height != dimensions.ExpectedHeight {
t.Fatalf(
"exported dimensions %.1fx%.1f do not match validated dimensions %.1fx%.1f",
dimensions.Width,
dimensions.Height,
dimensions.ExpectedWidth,
dimensions.ExpectedHeight,
)
}
}

Expand Down Expand Up @@ -300,6 +322,9 @@ func TestSVGEdgeLabelsAvoidElements(t *testing.T) {
if collisions := inspectEdgeLabelCollisions(t, path); len(collisions) > 0 {
t.Fatalf("relationship labels overlap diagram elements: %v", collisions)
}
if overflows := inspectEdgeLabelCanvasOverflows(t, path); len(overflows) > 0 {
t.Fatalf("relationship labels exceed the exported SVG canvas: %v", overflows)
}
}

func inspectNodeTextFit(t *testing.T, path string) ([]nodeOverflow, float64, []string, int) {
Expand Down Expand Up @@ -416,6 +441,40 @@ func inspectEdgeLabelCollisions(t *testing.T, path string) []string {
return collisions
}

// inspectEdgeLabelCanvasOverflows returns labels clipped by the exported SVG.
func inspectEdgeLabelCanvasOverflows(t *testing.T, path string) []string {
t.Helper()
testContext, cleanup := newChromeContext(t)
defer cleanup()

var overflows []string
fileURL := (&url.URL{Scheme: "file", Path: path}).String()
const overflowScript = `(() => {
const tolerance = 0.5;
const canvas = document.querySelector("svg#graph").getBoundingClientRect();
return [...document.querySelectorAll("g.edge")].flatMap((edge) => {
const label = edge.querySelector(":scope > rect");
if (!label) return [];
const rect = label.getBoundingClientRect();
if (rect.left >= canvas.left - tolerance &&
rect.top >= canvas.top - tolerance &&
rect.right <= canvas.right + tolerance &&
rect.bottom <= canvas.bottom + tolerance) {
return [];
}
return [edge.querySelector(":scope > text")?.textContent.trim() || edge.id];
});
})()`
if err := chromedp.Run(testContext,
chromedp.Navigate(fileURL),
chromedp.WaitVisible("g.edge", chromedp.ByQuery),
chromedp.Evaluate(overflowScript, &overflows),
); err != nil {
t.Fatalf("inspect relationship label canvas bounds: %v", err)
}
return overflows
}

func inspectNodeOrientation(t *testing.T, path string) string {
t.Helper()
testContext, cleanup := newChromeContext(t)
Expand Down
2 changes: 1 addition & 1 deletion cmd/mdl/webapp/dist/286.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cmd/mdl/webapp/dist/286.js.map

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions cmd/mdl/webapp/src/graph-view/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,9 @@ export class GraphData {
exportElastic.remove()
}

// Calculate actual content bounds including all elements
const contentBounds = this.calculateContentBounds()
// Export the exact bounds that passed geometry validation. Recomputing
// them here can omit routed relationship labels from the SVG canvas.
const contentBounds = this.validatedLayout.bounds

// Add padding around content
const padding = 50
Expand Down
Loading