- Node Title
+ @(Node.Title ?? "Title")
@foreach (var port in Node.Ports)
{
diff --git a/tests/Blazor.Diagrams.Core.Tests/Behaviors/EventsBehaviorTests.cs b/tests/Blazor.Diagrams.Core.Tests/Behaviors/EventsBehaviorTests.cs
new file mode 100644
index 00000000..6a87456c
--- /dev/null
+++ b/tests/Blazor.Diagrams.Core.Tests/Behaviors/EventsBehaviorTests.cs
@@ -0,0 +1,94 @@
+using Blazor.Diagrams.Core.Behaviors;
+using FluentAssertions;
+using Microsoft.AspNetCore.Components.Web;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Blazor.Diagrams.Core.Tests.Behaviors
+{
+ public class EventsBehaviorTests
+ {
+ [Fact]
+ public void Behavior_ShouldNotTriggerMouseClick_WhenItsRemoved()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ diagram.UnregisterBehavior
();
+ var eventTriggered = false;
+
+ // Act
+ diagram.MouseClick += (m, e) => eventTriggered = true;
+ diagram.OnMouseDown(null, new MouseEventArgs());
+ diagram.OnMouseUp(null, new MouseEventArgs());
+
+ // Assert
+ eventTriggered.Should().BeFalse();
+ }
+
+ [Fact]
+ public void Behavior_ShouldTriggerMouseClick_WhenMouseDownThenUpWithoutMove()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ var eventTriggered = false;
+
+ // Act
+ diagram.MouseClick += (m, e) => eventTriggered = true;
+ diagram.OnMouseDown(null, new MouseEventArgs());
+ diagram.OnMouseUp(null, new MouseEventArgs());
+
+ // Assert
+ eventTriggered.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Behavior_ShouldNotTriggerMouseClick_WhenMouseMoves()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ var eventTriggered = false;
+
+ // Act
+ diagram.MouseClick += (m, e) => eventTriggered = true;
+ diagram.OnMouseDown(null, new MouseEventArgs());
+ diagram.OnMouseMove(null, new MouseEventArgs());
+ diagram.OnMouseUp(null, new MouseEventArgs());
+
+ // Assert
+ eventTriggered.Should().BeFalse();
+ }
+
+ [Fact]
+ public void Behavior_ShouldTriggerMouseDoubleClick_WhenTwoMouseClicksHappenWithinTime()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ var eventTriggered = false;
+
+ // Act
+ diagram.MouseDoubleClick += (m, e) => eventTriggered = true;
+ diagram.OnMouseClick(null, new MouseEventArgs());
+ diagram.OnMouseClick(null, new MouseEventArgs());
+
+ // Assert
+ eventTriggered.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task Behavior_ShouldNotTriggerMouseDoubleClick_WhenTimeExceeds500()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ var eventTriggered = false;
+
+ // Act
+ diagram.MouseDoubleClick += (m, e) => eventTriggered = true;
+ diagram.OnMouseClick(null, new MouseEventArgs());
+ await Task.Delay(520);
+ diagram.OnMouseClick(null, new MouseEventArgs());
+
+ // Assert
+ eventTriggered.Should().BeFalse();
+ }
+ }
+}
diff --git a/tests/Blazor.Diagrams.Core.Tests/DiagramTests.cs b/tests/Blazor.Diagrams.Core.Tests/DiagramTests.cs
new file mode 100644
index 00000000..44079e78
--- /dev/null
+++ b/tests/Blazor.Diagrams.Core.Tests/DiagramTests.cs
@@ -0,0 +1,95 @@
+using Blazor.Diagrams.Core.Geometry;
+using Blazor.Diagrams.Core.Models;
+using FluentAssertions;
+using Xunit;
+
+namespace Blazor.Diagrams.Core.Tests
+{
+ public class DiagramTests
+ {
+ [Fact]
+ public void GetScreenPoint_ShouldReturnCorrectPoint()
+ {
+ // Arrange
+ var diagram = new Diagram();
+
+ // Act
+ diagram.SetZoom(1.234);
+ diagram.UpdatePan(50, 50);
+ diagram.SetContainer(new Rectangle(30, 65, 1000, 793));
+ var pt = diagram.GetScreenPoint(100, 200);
+
+ // Assert
+ pt.X.Should().Be(203.4); // 2*X + panX + left
+ pt.Y.Should().Be(361.8); // 2*Y + panY + top
+ }
+
+ [Fact]
+ public void ZoomToFit_ShouldUseSelectedNodesIfAny()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ diagram.SetContainer(new Rectangle(new Point(0, 0), new Size(1080, 768)));
+ diagram.Nodes.Add(new NodeModel(new Point(50, 50))
+ {
+ Size = new Size(100, 80)
+ });
+ diagram.SelectModel(diagram.Nodes[0], true);
+
+ // Act
+ diagram.ZoomToFit(10);
+
+ // Assert
+ diagram.Zoom.Should().BeApproximately(7.68, 0.001);
+ diagram.Pan.X.Should().Be(-40);
+ diagram.Pan.Y.Should().Be(-40);
+ }
+
+ [Fact]
+ public void ZoomToFit_ShouldUseNodesWhenNoneSelected()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ diagram.SetContainer(new Rectangle(new Point(0, 0), new Size(1080, 768)));
+ diagram.Nodes.Add(new NodeModel(new Point(50, 50))
+ {
+ Size = new Size(100, 80)
+ });
+
+ // Act
+ diagram.ZoomToFit(10);
+
+ // Assert
+ diagram.Zoom.Should().BeApproximately(7.68, 0.001);
+ diagram.Pan.X.Should().Be(-40);
+ diagram.Pan.Y.Should().Be(-40);
+ }
+
+ [Fact]
+ public void ZoomToFit_ShouldTriggerAppropriateEvents()
+ {
+ // Arrange
+ var diagram = new Diagram();
+ diagram.SetContainer(new Rectangle(new Point(0, 0), new Size(1080, 768)));
+ diagram.Nodes.Add(new NodeModel(new Point(50, 50))
+ {
+ Size = new Size(100, 80)
+ });
+
+ var refreshes = 0;
+ var zoomChanges = 0;
+ var panChanges = 0;
+
+ // Act
+ diagram.Changed += () => refreshes++;
+ diagram.ZoomChanged += () => zoomChanges++;
+ diagram.PanChanged += () => panChanges++;
+ diagram.ZoomToFit(10);
+
+ // Assert
+ refreshes.Should().Be(1);
+ zoomChanges.Should().Be(1);
+ panChanges.Should().Be(1);
+ }
+ }
+}
diff --git a/tests/Blazor.Diagrams.Core.Tests/Models/Base/BaseLinkModelTests.cs b/tests/Blazor.Diagrams.Core.Tests/Models/Base/BaseLinkModelTests.cs
new file mode 100644
index 00000000..eedfe8d7
--- /dev/null
+++ b/tests/Blazor.Diagrams.Core.Tests/Models/Base/BaseLinkModelTests.cs
@@ -0,0 +1,93 @@
+using Blazor.Diagrams.Core.Models;
+using Blazor.Diagrams.Core.Models.Base;
+using FluentAssertions;
+using Xunit;
+
+namespace Blazor.Diagrams.Core.Tests.Models.Base
+{
+ public class BaseLinkModelTests
+ {
+ [Fact]
+ public void SetSourcePort_ShouldChangePropertiesAndTriggerEvent()
+ {
+ // Arrange
+ var link = new TestLink(sourcePort: new PortModel(null), targetPort: null);
+ var parent = new NodeModel();
+ var sp = new PortModel(parent);
+ var eventsTriggered = 0;
+ PortModel oldSp = null;
+ PortModel newSp = null;
+ BaseLinkModel linkInstance = null;
+
+ // Act
+ link.SourcePortChanged += (l, o, n) =>
+ {
+ eventsTriggered++;
+ linkInstance = l;
+ oldSp = o;
+ newSp = n;
+ };
+
+ link.SetSourcePort(sp);
+
+ // Assert
+ eventsTriggered.Should().Be(1);
+ link.SourcePort.Should().BeSameAs(sp);
+ oldSp.Should().NotBeNull();
+ newSp.Should().BeSameAs(sp);
+ linkInstance.Should().BeSameAs(link);
+ link.SourceNode.Should().BeSameAs(parent);
+ }
+
+ [Fact]
+ public void SetTargetPort_ShouldChangePropertiesAndTriggerEvent()
+ {
+ // Arrange
+ var link = new TestLink(sourcePort: new PortModel(null), targetPort: null);
+ var parent = new NodeModel();
+ var tp = new PortModel(parent);
+ var eventsTriggered = 0;
+ PortModel oldTp = null;
+ PortModel newTp = null;
+ BaseLinkModel linkInstance = null;
+
+ // Act
+ link.TargetPortChanged += (l, o, n) =>
+ {
+ eventsTriggered++;
+ linkInstance = l;
+ oldTp = o;
+ newTp = n;
+ };
+
+ link.SetTargetPort(tp);
+
+ // Assert
+ eventsTriggered.Should().Be(1);
+ link.TargetPort.Should().BeSameAs(tp);
+ oldTp.Should().BeNull();
+ newTp.Should().BeSameAs(tp);
+ linkInstance.Should().BeSameAs(link);
+ link.TargetNode.Should().BeSameAs(parent);
+ }
+
+ private class TestLink : BaseLinkModel
+ {
+ public TestLink(NodeModel sourceNode, NodeModel targetNode) : base(sourceNode, targetNode)
+ {
+ }
+
+ public TestLink(PortModel sourcePort, PortModel targetPort = null) : base(sourcePort, targetPort)
+ {
+ }
+
+ public TestLink(string id, NodeModel sourceNode, NodeModel targetNode) : base(id, sourceNode, targetNode)
+ {
+ }
+
+ public TestLink(string id, PortModel sourcePort, PortModel targetPort = null) : base(id, sourcePort, targetPort)
+ {
+ }
+ }
+ }
+}