diff --git a/Directory.Packages.props b/Directory.Packages.props index c9d9779..79b05c7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,12 +4,13 @@ - + + \ No newline at end of file diff --git a/NuGet.config b/NuGet.config index 3ca7599..cf92140 100644 --- a/NuGet.config +++ b/NuGet.config @@ -17,7 +17,7 @@ hashAlgorithm="SHA256" allowUntrustedRoot="false" /> - DendroDocs;microsoft;nuget;simoncropp;nukebuild;SharpDevelop;rsuter;serilog;jetbrains;mrunleaded;dotnetframework;danielpalme;aaubry;gittools;GitHub + DendroDocs;eNeRGy164;microsoft;nuget;simoncropp;nukebuild;SharpDevelop;rsuter;serilog;jetbrains;mrunleaded;dotnetframework;danielpalme;aaubry;gittools;GitHub diff --git a/src/DendroDocs.Client/DendroDocs.Client.csproj b/src/DendroDocs.Client/DendroDocs.Client.csproj index c4780e2..9fe6b98 100644 --- a/src/DendroDocs.Client/DendroDocs.Client.csproj +++ b/src/DendroDocs.Client/DendroDocs.Client.csproj @@ -30,6 +30,7 @@ + diff --git a/src/DendroDocs.Client/Uml/Extensions/IHaveModifiersExtensions.cs b/src/DendroDocs.Client/Uml/Extensions/IHaveModifiersExtensions.cs new file mode 100644 index 0000000..5345880 --- /dev/null +++ b/src/DendroDocs.Client/Uml/Extensions/IHaveModifiersExtensions.cs @@ -0,0 +1,50 @@ +using DendroDocs.Extensions; +using PlantUml.Builder; + +namespace DendroDocs.Uml.Extensions; + +/// +/// Provides extension methods for objects implementing the interface, enabling conversion +/// of visibility modifiers to their PlantUML equivalents. +/// +/// This class contains methods that simplify the process of mapping visibility modifiers from objects +/// implementing to corresponding UML visibility representations, such as public, +/// protected, private, and internal. +public static class IHaveModifiersExtensions +{ + /// + /// Converts the visibility modifiers of the specified object to a PlantUML visibility modifier. + /// + /// An object implementing that provides access to visibility modifiers. + /// A value representing the UML visibility equivalent of the object's modifiers. + /// Returns for public visibility, for internal visibility, + /// for protected visibility, for private visibility, or if no visibility modifier is applicable. + public static VisibilityModifier ToPlantUmlVisibility(this IHaveModifiers modifiers) + { + ArgumentNullException.ThrowIfNull(modifiers); + + if (modifiers.IsPublic()) + { + return VisibilityModifier.Public; + } + + if (modifiers.IsInternal()) + { + return VisibilityModifier.PackagePrivate; + } + + if (modifiers.IsProtected()) + { + return VisibilityModifier.Protected; + } + + if (modifiers.IsPrivate()) + { + return VisibilityModifier.Private; + } + + return VisibilityModifier.None; + } +} diff --git a/src/DendroDocs.Client/Uml/Fragments/Alt.cs b/src/DendroDocs.Client/Uml/Fragments/Alt.cs new file mode 100644 index 0000000..49dd690 --- /dev/null +++ b/src/DendroDocs.Client/Uml/Fragments/Alt.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; + +namespace DendroDocs.Uml.Fragments; + +/// +/// Represents a group with 1 or more sections, like alt, par, loop, etc.. +/// +[DebuggerDisplay("Alt")] +public class Alt : InteractionFragment +{ + private readonly List sections = []; + + /// + /// Gets all sections. + /// + public IReadOnlyList Sections => this.sections; + + /// + /// Add a sections to this alt. + /// + /// The section to add. + public void AddSection(AltSection section) + { + ArgumentNullException.ThrowIfNull(section); + + section.Parent = this; + + this.sections.Add(section); + } +} diff --git a/src/DendroDocs.Client/Uml/Fragments/AltSection.cs b/src/DendroDocs.Client/Uml/Fragments/AltSection.cs new file mode 100644 index 0000000..c1a3d4b --- /dev/null +++ b/src/DendroDocs.Client/Uml/Fragments/AltSection.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; + +namespace DendroDocs.Uml.Fragments; + +/// +/// Represents a section in a larger group. +/// +[DebuggerDisplay("AltSection {GroupType} {Label}")] +public class AltSection : InteractionFragment +{ + /// + /// The type of group, like alt, else, etc.. + /// + public string? GroupType { get; set; } + + /// + /// The label of this section. + /// + public string? Label { get; set; } +} diff --git a/src/DendroDocs.Client/Uml/Fragments/Arrow.cs b/src/DendroDocs.Client/Uml/Fragments/Arrow.cs new file mode 100644 index 0000000..fb2bd6e --- /dev/null +++ b/src/DendroDocs.Client/Uml/Fragments/Arrow.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; + +namespace DendroDocs.Uml.Fragments; + +/// +/// Represents an arrow between 2 participants. +/// +[DebuggerDisplay("{Source} -> {Target} : {Name}")] +public class Arrow : InteractionFragment +{ + /// + /// The left participant of the arrow. + /// + public string? Source { get; set; } + + /// + /// The right participant of the arrow. + /// + public string? Target { get; set; } + + /// + /// The optional color of the arrow. + /// + public string? Color { get; set; } + + /// + /// Whether the arrow is dashed. + /// + public bool Dashed { get; set; } + + /// + /// The message with the arrow. + /// + public string? Name { get; set; } +} diff --git a/src/DendroDocs.Client/Uml/Fragments/Extensions/InteractionFragmentExtensions.cs b/src/DendroDocs.Client/Uml/Fragments/Extensions/InteractionFragmentExtensions.cs new file mode 100644 index 0000000..339b2f9 --- /dev/null +++ b/src/DendroDocs.Client/Uml/Fragments/Extensions/InteractionFragmentExtensions.cs @@ -0,0 +1,80 @@ +namespace DendroDocs.Uml.Fragments.Extensions; + +/// +/// Provides extension methods for querying and navigating through collections of +/// objects. +/// +/// This static class includes methods for retrieving descendants, ancestors, and sibling fragments +/// within a hierarchy of objects. These methods are designed to facilitate traversal +/// and filtering of interaction fragments in a structured manner. +public static class InteractionFragmentExtensions +{ + /// + /// Query all descendants from this level down. + /// + /// The type of fragment to filter on. + /// Returns a readonly list of child fragments. + public static IReadOnlyList Descendants(this IEnumerable nodes) + where TFragment : InteractionFragment + { + ArgumentNullException.ThrowIfNull(nodes); + + var result = new List(); + + foreach (var node in nodes) + { + switch (node) + { + case TFragment t: + result.Add(t); + break; + + case Alt a: + result.AddRange(a.Sections.SelectMany(s => s.Fragments).Descendants()); + break; + + default: + break; + } + } + + return result; + } + + /// + /// Query all parent fragments from this fragment up. + /// + /// Returns a list of parent fragments. + public static IReadOnlyList Ancestors(this InteractionFragment fragment) + { + ArgumentNullException.ThrowIfNull(fragment); + + var result = new List(); + + var parent = fragment.Parent; + while (parent is not null) + { + result.Add(parent); + + parent = parent.Parent; + } + + return result; + } + + /// + /// Query all sibling before the current fragment. + /// + /// Returns a readonly list of siblings comming before this fragment. + public static IReadOnlyList StatementsBeforeSelf(this InteractionFragment fragment) + { + ArgumentNullException.ThrowIfNull(fragment); + + if (fragment.Parent != null) + { + return [.. fragment.Parent.Fragments.TakeWhile(s => s != fragment)]; + } + + return []; + } +} diff --git a/src/DendroDocs.Client/Uml/Fragments/InteractionFragment.cs b/src/DendroDocs.Client/Uml/Fragments/InteractionFragment.cs new file mode 100644 index 0000000..42fd1ab --- /dev/null +++ b/src/DendroDocs.Client/Uml/Fragments/InteractionFragment.cs @@ -0,0 +1,46 @@ +namespace DendroDocs.Uml.Fragments; + +/// +/// Represents interaction fragments in a sequence diagram. +/// +public abstract class InteractionFragment +{ + private readonly List interactionFragments = []; + + /// + /// The parent of this fragment. + /// + public InteractionFragment? Parent { get; set; } + + /// + /// The children of this fragment. + /// + public virtual IReadOnlyList Fragments => this.interactionFragments; + + /// + /// Add a fragment to this level. + /// + /// The fragment to add. + public void AddFragment(InteractionFragment fragment) + { + ArgumentNullException.ThrowIfNull(fragment); + + fragment.Parent = this; + + this.interactionFragments.Add(fragment); + } + + /// + /// Add a list of fragments to this level. + /// + /// The fragments to add. + public void AddFragments(IEnumerable fragments) + { + ArgumentNullException.ThrowIfNull(fragments); + + foreach (var fragment in fragments) + { + this.AddFragment(fragment); + } + } +} diff --git a/src/DendroDocs.Client/Uml/Fragments/Interactions.cs b/src/DendroDocs.Client/Uml/Fragments/Interactions.cs new file mode 100644 index 0000000..0e0e288 --- /dev/null +++ b/src/DendroDocs.Client/Uml/Fragments/Interactions.cs @@ -0,0 +1,8 @@ +namespace DendroDocs.Uml.Fragments; + +/// +/// Represents a list of fragments on the same level. +/// +public class Interactions : InteractionFragment +{ +} diff --git a/src/DendroDocs.Client/packages.lock.json b/src/DendroDocs.Client/packages.lock.json index d74932a..ed71f51 100644 --- a/src/DendroDocs.Client/packages.lock.json +++ b/src/DendroDocs.Client/packages.lock.json @@ -4,9 +4,9 @@ "net8.0": { "DendroDocs.Shared": { "type": "Direct", - "requested": "[0.3.2, )", - "resolved": "0.3.2", - "contentHash": "8k305PV9XQwZle+1EXH1ckGc/QJRYq/6eQOz2W6EFM0arobYLbcub1aCKiW6nZ7LGXdwEsB/WOEJjZ+sd6j+9w==", + "requested": "[0.4.2, )", + "resolved": "0.4.2", + "contentHash": "YfYo/kboAwdy8Qo0Iu1FF7+pZ+Fd8BHIyM8XbHTczjYjcmVsSJl2VS1oCJTj5eymMHqWMX2eTzQWRMByxnVVaA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } @@ -21,6 +21,12 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "PlantUml.Builder": { + "type": "Direct", + "requested": "[0.1.63, )", + "resolved": "0.1.63", + "contentHash": "oidQGXTbe3E9SQ3UWLtUglJNgGEg2hWyCc4vDU1xYtESG9hZoO93tRu2QIhH/KCGSBh50hMuOMRXBUHoIyiJbQ==" + }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "8.0.0", diff --git a/tests/DendroDocs.Client.Tests/Uml/Extensions/IHaveModifiersExtensionsTests.cs b/tests/DendroDocs.Client.Tests/Uml/Extensions/IHaveModifiersExtensionsTests.cs new file mode 100644 index 0000000..a1ac5a2 --- /dev/null +++ b/tests/DendroDocs.Client.Tests/Uml/Extensions/IHaveModifiersExtensionsTests.cs @@ -0,0 +1,157 @@ + +using PlantUml.Builder; + +namespace DendroDocs.Uml.Extensions.Tests; + +[TestClass] +public class IHaveModifiersExtensionsTests +{ + [TestMethod] + public void ToPlantUmlVisibility_NullModifiers_Should_Throw() + { + // Arrange + IHaveModifiers modifiers = null!; + + // Act + Action action = () => modifiers.ToPlantUmlVisibility(); + + // Assert + action.ShouldThrow(); + } + + [TestMethod] + public void ToPlantUmlVisibility_PublicModifier_Should_ReturnPublic() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Public }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.Public); + } + + [TestMethod] + public void ToPlantUmlVisibility_InternalModifier_Should_ReturnPackagePrivate() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Internal }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.PackagePrivate); + } + + [TestMethod] + public void ToPlantUmlVisibility_ProtectedModifier_Should_ReturnProtected() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Protected }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.Protected); + } + + [TestMethod] + public void ToPlantUmlVisibility_PrivateModifier_Should_ReturnPrivate() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Private }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.Private); + } + + [TestMethod] + public void ToPlantUmlVisibility_NoVisibilityModifier_Should_ReturnNone() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Static }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.None); + } + + [TestMethod] + public void ToPlantUmlVisibility_NoModifiers_Should_ReturnNone() + { + // Arrange + var modifiers = new TestModifiers(); + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.None); + } + + [TestMethod] + public void ToPlantUmlVisibility_CombinedModifiers_Should_ReturnFirstVisibilityMatch() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Public | Modifier.Static }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.Public); + } + + [TestMethod] + public void ToPlantUmlVisibility_PriorityOrder_Should_ReturnPublicFirst() + { + // Arrange - This scenario shouldn't occur in real code, but tests the method's priority + var modifiers = new TestModifiers { Modifiers = Modifier.Public | Modifier.Private }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.Public); + } + + [TestMethod] + public void ToPlantUmlVisibility_InternalOverProtected_Should_ReturnPackagePrivate() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Internal | Modifier.Protected }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.PackagePrivate); + } + + [TestMethod] + public void ToPlantUmlVisibility_ProtectedOverPrivate_Should_ReturnProtected() + { + // Arrange + var modifiers = new TestModifiers { Modifiers = Modifier.Protected | Modifier.Private }; + + // Act + var result = modifiers.ToPlantUmlVisibility(); + + // Assert + result.ShouldBe(VisibilityModifier.Protected); + } + + // Helper class for testing IHaveModifiers + private class TestModifiers : IHaveModifiers + { + public Modifier Modifiers { get; set; } + } +} diff --git a/tests/DendroDocs.Client.Tests/Uml/Fragments/AltSectionTests.cs b/tests/DendroDocs.Client.Tests/Uml/Fragments/AltSectionTests.cs new file mode 100644 index 0000000..3fd23b8 --- /dev/null +++ b/tests/DendroDocs.Client.Tests/Uml/Fragments/AltSectionTests.cs @@ -0,0 +1,136 @@ +namespace DendroDocs.Uml.Fragments.Tests; + +[TestClass] +public class AltSectionTests +{ + [TestMethod] + public void Constructor_Should_InitializeWithNullProperties() + { + // Act + var section = new AltSection(); + + // Assert + section.GroupType.ShouldBeNull(); + section.Label.ShouldBeNull(); + section.Fragments.ShouldBeEmpty(); + } + + [TestMethod] + public void GroupType_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var section = new AltSection(); + const string groupType = "alt"; + + // Act + section.GroupType = groupType; + + // Assert + section.GroupType.ShouldBe(groupType); + } + + [TestMethod] + public void Label_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var section = new AltSection(); + const string label = "condition1"; + + // Act + section.Label = label; + + // Assert + section.Label.ShouldBe(label); + } + + [TestMethod] + public void GroupType_SetToNull_Should_AcceptNull() + { + // Arrange + var section = new AltSection { GroupType = "alt" }; + + // Act + section.GroupType = null; + + // Assert + section.GroupType.ShouldBeNull(); + } + + [TestMethod] + public void Label_SetToNull_Should_AcceptNull() + { + // Arrange + var section = new AltSection { Label = "condition" }; + + // Act + section.Label = null; + + // Assert + section.Label.ShouldBeNull(); + } + + [TestMethod] + public void AddFragment_Should_WorkAsInteractionFragment() + { + // Arrange + var section = new AltSection { GroupType = "alt", Label = "condition" }; + var arrow = new Arrow { Source = "A", Target = "B", Name = "message" }; + + // Act + section.AddFragment(arrow); + + // Assert + section.Fragments.ShouldHaveSingleItem(); + section.Fragments[0].ShouldBe(arrow); + arrow.Parent.ShouldBe(section); + } + + [TestMethod] + public void InheritsFromInteractionFragment_Should_HaveFragmentBehavior() + { + // Arrange + var alt = new Alt(); + var section = new AltSection { GroupType = "alt", Label = "condition" }; + + // Act + alt.AddSection(section); + + // Assert + section.Parent.ShouldBe(alt); + alt.Sections.ShouldHaveSingleItem(); + alt.Sections[0].ShouldBe(section); + } + + [TestMethod] + public void DebuggerDisplay_Should_ShowCorrectFormat() + { + // Arrange + var section = new AltSection { GroupType = "alt", Label = "x > 0" }; + + // Act & Assert + // Note: We can't directly test DebuggerDisplay attribute, but we can verify the properties exist + section.GroupType.ShouldBe("alt"); + section.Label.ShouldBe("x > 0"); + } + + [TestMethod] + public void AddFragments_MultipleLevels_Should_BuildHierarchy() + { + // Arrange + var section = new AltSection { GroupType = "alt", Label = "condition" }; + var nestedAlt = new Alt(); + var nestedSection = new AltSection { GroupType = "opt", Label = "nested" }; + var arrow = new Arrow { Source = "A", Target = "B" }; + + // Act + nestedSection.AddFragment(arrow); + nestedAlt.AddSection(nestedSection); + section.AddFragment(nestedAlt); + + // Assert + section.Fragments.ShouldHaveSingleItem(); + nestedAlt.Parent.ShouldBe(section); + nestedSection.Parent.ShouldBe(nestedAlt); + arrow.Parent.ShouldBe(nestedSection); + } +} diff --git a/tests/DendroDocs.Client.Tests/Uml/Fragments/AltTests.cs b/tests/DendroDocs.Client.Tests/Uml/Fragments/AltTests.cs new file mode 100644 index 0000000..a731c92 --- /dev/null +++ b/tests/DendroDocs.Client.Tests/Uml/Fragments/AltTests.cs @@ -0,0 +1,103 @@ +namespace DendroDocs.Uml.Fragments.Tests; + +[TestClass] +public class AltTests +{ + [TestMethod] + public void Constructor_Should_InitializeEmptySections() + { + // Act + var alt = new Alt(); + + // Assert + alt.Sections.ShouldNotBeNull(); + alt.Sections.ShouldBeEmpty(); + } + + [TestMethod] + public void AddSection_ValidSection_Should_AddToSections() + { + // Arrange + var alt = new Alt(); + var section = new AltSection { GroupType = "alt", Label = "condition" }; + + // Act + alt.AddSection(section); + + // Assert + alt.Sections.ShouldHaveSingleItem(); + alt.Sections[0].ShouldBe(section); + section.Parent.ShouldBe(alt); + } + + [TestMethod] + public void AddSection_NullSection_Should_Throw() + { + // Arrange + var alt = new Alt(); + + // Act + Action action = () => alt.AddSection(null!); + + // Assert + action.ShouldThrow(); + } + + [TestMethod] + public void AddSection_MultipleSections_Should_MaintainOrder() + { + // Arrange + var alt = new Alt(); + var section1 = new AltSection { GroupType = "alt", Label = "condition1" }; + var section2 = new AltSection { GroupType = "else", Label = "condition2" }; + var section3 = new AltSection { GroupType = "else", Label = "condition3" }; + + // Act + alt.AddSection(section1); + alt.AddSection(section2); + alt.AddSection(section3); + + // Assert + alt.Sections.Count.ShouldBe(3); + alt.Sections[0].ShouldBe(section1); + alt.Sections[1].ShouldBe(section2); + alt.Sections[2].ShouldBe(section3); + } + + [TestMethod] + public void AddSection_SectionWithFragments_Should_WorkCorrectly() + { + // Arrange + var alt = new Alt(); + var section = new AltSection { GroupType = "alt", Label = "condition" }; + var arrow = new Arrow { Source = "A", Target = "B", Name = "message" }; + section.AddFragment(arrow); + + // Act + alt.AddSection(section); + + // Assert + alt.Sections.ShouldHaveSingleItem(); + alt.Sections[0].Fragments.ShouldHaveSingleItem(); + alt.Sections[0].Fragments[0].ShouldBe(arrow); + section.Parent.ShouldBe(alt); + } + + [TestMethod] + public void InheritsFromInteractionFragment_Should_HaveFragmentBehavior() + { + // Arrange + var parentFragment = new Interactions(); + var alt = new Alt(); + var section = new AltSection { GroupType = "alt", Label = "condition" }; + alt.AddSection(section); + + // Act + parentFragment.AddFragment(alt); + + // Assert + alt.Parent.ShouldBe(parentFragment); + parentFragment.Fragments.ShouldHaveSingleItem(); + parentFragment.Fragments[0].ShouldBe(alt); + } +} diff --git a/tests/DendroDocs.Client.Tests/Uml/Fragments/ArrowTests.cs b/tests/DendroDocs.Client.Tests/Uml/Fragments/ArrowTests.cs new file mode 100644 index 0000000..3918f40 --- /dev/null +++ b/tests/DendroDocs.Client.Tests/Uml/Fragments/ArrowTests.cs @@ -0,0 +1,184 @@ +namespace DendroDocs.Uml.Fragments.Tests; + +[TestClass] +public class ArrowTests +{ + [TestMethod] + public void Constructor_Should_InitializeWithNullProperties() + { + // Act + var arrow = new Arrow(); + + // Assert + arrow.Source.ShouldBeNull(); + arrow.Target.ShouldBeNull(); + arrow.Color.ShouldBeNull(); + arrow.Name.ShouldBeNull(); + arrow.Dashed.ShouldBeFalse(); + } + + [TestMethod] + public void Source_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var arrow = new Arrow(); + const string source = "ParticipantA"; + + // Act + arrow.Source = source; + + // Assert + arrow.Source.ShouldBe(source); + } + + [TestMethod] + public void Target_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var arrow = new Arrow(); + const string target = "ParticipantB"; + + // Act + arrow.Target = target; + + // Assert + arrow.Target.ShouldBe(target); + } + + [TestMethod] + public void Color_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var arrow = new Arrow(); + const string color = "red"; + + // Act + arrow.Color = color; + + // Assert + arrow.Color.ShouldBe(color); + } + + [TestMethod] + public void Name_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var arrow = new Arrow(); + const string name = "getMessage()"; + + // Act + arrow.Name = name; + + // Assert + arrow.Name.ShouldBe(name); + } + + [TestMethod] + public void Dashed_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var arrow = new Arrow(); + + // Act + arrow.Dashed = true; + + // Assert + arrow.Dashed.ShouldBeTrue(); + } + + [TestMethod] + public void AllProperties_SetToNull_Should_AcceptNull() + { + // Arrange + var arrow = new Arrow + { + Source = "A", + Target = "B", + Color = "blue", + Name = "message" + }; + + // Act + arrow.Source = null; + arrow.Target = null; + arrow.Color = null; + arrow.Name = null; + + // Assert + arrow.Source.ShouldBeNull(); + arrow.Target.ShouldBeNull(); + arrow.Color.ShouldBeNull(); + arrow.Name.ShouldBeNull(); + } + + [TestMethod] + public void InheritsFromInteractionFragment_Should_HaveFragmentBehavior() + { + // Arrange + var interactions = new Interactions(); + var arrow = new Arrow { Source = "A", Target = "B", Name = "message" }; + + // Act + interactions.AddFragment(arrow); + + // Assert + arrow.Parent.ShouldBe(interactions); + interactions.Fragments.ShouldHaveSingleItem(); + interactions.Fragments[0].ShouldBe(arrow); + } + + [TestMethod] + public void DebuggerDisplay_Should_ShowCorrectFormat() + { + // Arrange + var arrow = new Arrow + { + Source = "ClientA", + Target = "ServerB", + Name = "processRequest()" + }; + + // Act & Assert + // Note: We can't directly test DebuggerDisplay attribute, but we can verify the properties exist + arrow.Source.ShouldBe("ClientA"); + arrow.Target.ShouldBe("ServerB"); + arrow.Name.ShouldBe("processRequest()"); + } + + [TestMethod] + public void Arrow_CanBeAddedToAltSection() + { + // Arrange + var section = new AltSection { GroupType = "alt", Label = "condition" }; + var arrow = new Arrow { Source = "A", Target = "B", Name = "call" }; + + // Act + section.AddFragment(arrow); + + // Assert + section.Fragments.ShouldHaveSingleItem(); + section.Fragments[0].ShouldBe(arrow); + arrow.Parent.ShouldBe(section); + } + + [TestMethod] + public void MultipleArrows_InSequence_Should_MaintainOrder() + { + // Arrange + var interactions = new Interactions(); + var arrow1 = new Arrow { Source = "A", Target = "B", Name = "first" }; + var arrow2 = new Arrow { Source = "B", Target = "C", Name = "second" }; + var arrow3 = new Arrow { Source = "C", Target = "A", Name = "third" }; + + // Act + interactions.AddFragment(arrow1); + interactions.AddFragment(arrow2); + interactions.AddFragment(arrow3); + + // Assert + interactions.Fragments.Count.ShouldBe(3); + interactions.Fragments[0].ShouldBe(arrow1); + interactions.Fragments[1].ShouldBe(arrow2); + interactions.Fragments[2].ShouldBe(arrow3); + } +} diff --git a/tests/DendroDocs.Client.Tests/Uml/Fragments/Extensions/InteractionFragmentExtensionsTests.cs b/tests/DendroDocs.Client.Tests/Uml/Fragments/Extensions/InteractionFragmentExtensionsTests.cs new file mode 100644 index 0000000..00221e5 --- /dev/null +++ b/tests/DendroDocs.Client.Tests/Uml/Fragments/Extensions/InteractionFragmentExtensionsTests.cs @@ -0,0 +1,293 @@ +namespace DendroDocs.Uml.Fragments.Extensions.Tests; + +[TestClass] +public class InteractionFragmentExtensionsTests +{ + [TestMethod] + public void Descendants_NullNodes_Should_Throw() + { + // Arrange + IEnumerable nodes = null!; + + // Act + Action action = () => nodes.Descendants(); + + // Assert + action.ShouldThrow(); + } + + [TestMethod] + public void Descendants_EmptyCollection_Should_ReturnEmpty() + { + // Arrange + var nodes = Array.Empty(); + + // Act + var result = nodes.Descendants(); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void Descendants_DirectArrowMatches_Should_ReturnArrows() + { + // Arrange + var arrow1 = new Arrow { Source = "A", Target = "B" }; + var arrow2 = new Arrow { Source = "B", Target = "C" }; + var alt = new Alt(); + var nodes = new InteractionFragment[] { arrow1, alt, arrow2 }; + + // Act + var result = nodes.Descendants(); + + // Assert + result.Count.ShouldBe(2); + result.ShouldContain(arrow1); + result.ShouldContain(arrow2); + } + + [TestMethod] + public void Descendants_ArrowsInAltSections_Should_ReturnArrows() + { + // Arrange + var alt = new Alt(); + var section1 = new AltSection { GroupType = "alt" }; + var section2 = new AltSection { GroupType = "else" }; + var arrow1 = new Arrow { Source = "A", Target = "B" }; + var arrow2 = new Arrow { Source = "B", Target = "C" }; + + section1.AddFragment(arrow1); + section2.AddFragment(arrow2); + alt.AddSection(section1); + alt.AddSection(section2); + + var nodes = new InteractionFragment[] { alt }; + + // Act + var result = nodes.Descendants(); + + // Assert + result.Count.ShouldBe(2); + result.ShouldContain(arrow1); + result.ShouldContain(arrow2); + } + + [TestMethod] + public void Descendants_NestedAltStructures_Should_ReturnAllMatches() + { + // Arrange + var outerAlt = new Alt(); + var outerSection = new AltSection { GroupType = "alt" }; + var innerAlt = new Alt(); + var innerSection = new AltSection { GroupType = "opt" }; + var arrow = new Arrow { Source = "A", Target = "B" }; + + innerSection.AddFragment(arrow); + innerAlt.AddSection(innerSection); + outerSection.AddFragment(innerAlt); + outerAlt.AddSection(outerSection); + + var nodes = new InteractionFragment[] { outerAlt }; + + // Act + var result = nodes.Descendants(); + + // Assert + result.ShouldHaveSingleItem(); + result[0].ShouldBe(arrow); + } + + + [TestMethod] + public void Descendants_NonAltFragment_Should_NotTraverseChildren() + { + // Arrange + var interactions = new Interactions(); + var arrow = new Arrow { Source = "A", Target = "B" }; + interactions.AddFragment(arrow); + + var nodes = new InteractionFragment[] { interactions }; + + // Act + var result = nodes.Descendants(); + + // Assert + result.ShouldBeEmpty(); // Interactions is not handled in the switch, so children are not traversed + } + + [TestMethod] + public void Ancestors_NullFragment_Should_Throw() + { + // Arrange + InteractionFragment fragment = null!; + + // Act + Action action = () => fragment.Ancestors(); + + // Assert + action.ShouldThrow(); + } + + [TestMethod] + public void Ancestors_FragmentWithoutParent_Should_ReturnEmpty() + { + // Arrange + var arrow = new Arrow { Source = "A", Target = "B" }; + + // Act + var result = arrow.Ancestors(); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void Ancestors_FragmentWithSingleParent_Should_ReturnParent() + { + // Arrange + var interactions = new Interactions(); + var arrow = new Arrow { Source = "A", Target = "B" }; + interactions.AddFragment(arrow); + + // Act + var result = arrow.Ancestors(); + + // Assert + result.ShouldHaveSingleItem(); + result[0].ShouldBe(interactions); + } + + [TestMethod] + public void Ancestors_FragmentWithMultipleParents_Should_ReturnAllAncestors() + { + // Arrange + var interactions = new Interactions(); + var alt = new Alt(); + var section = new AltSection { GroupType = "alt" }; + var arrow = new Arrow { Source = "A", Target = "B" }; + + section.AddFragment(arrow); + alt.AddSection(section); + interactions.AddFragment(alt); + + // Act + var result = arrow.Ancestors(); + + // Assert + result.Count.ShouldBe(3); + result[0].ShouldBe(section); + result[1].ShouldBe(alt); + result[2].ShouldBe(interactions); + } + + [TestMethod] + public void StatementsBeforeSelf_NullFragment_Should_Throw() + { + // Arrange + InteractionFragment fragment = null!; + + // Act + Action action = () => fragment.StatementsBeforeSelf(); + + // Assert + action.ShouldThrow(); + } + + [TestMethod] + public void StatementsBeforeSelf_FragmentWithoutParent_Should_ReturnEmpty() + { + // Arrange + var arrow = new Arrow { Source = "A", Target = "B" }; + + // Act + var result = arrow.StatementsBeforeSelf(); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void StatementsBeforeSelf_FirstFragment_Should_ReturnEmpty() + { + // Arrange + var interactions = new Interactions(); + var arrow1 = new Arrow { Source = "A", Target = "B" }; + var arrow2 = new Arrow { Source = "B", Target = "C" }; + interactions.AddFragment(arrow1); + interactions.AddFragment(arrow2); + + // Act + var result = arrow1.StatementsBeforeSelf(); + + // Assert + result.ShouldBeEmpty(); + } + + [TestMethod] + public void StatementsBeforeSelf_SecondFragment_Should_ReturnFirst() + { + // Arrange + var interactions = new Interactions(); + var arrow1 = new Arrow { Source = "A", Target = "B" }; + var arrow2 = new Arrow { Source = "B", Target = "C" }; + interactions.AddFragment(arrow1); + interactions.AddFragment(arrow2); + + // Act + var result = arrow2.StatementsBeforeSelf(); + + // Assert + result.ShouldHaveSingleItem(); + result[0].ShouldBe(arrow1); + } + + [TestMethod] + public void StatementsBeforeSelf_LastFragment_Should_ReturnAllPrevious() + { + // Arrange + var interactions = new Interactions(); + var arrow1 = new Arrow { Source = "A", Target = "B" }; + var arrow2 = new Arrow { Source = "B", Target = "C" }; + var arrow3 = new Arrow { Source = "C", Target = "D" }; + interactions.AddFragment(arrow1); + interactions.AddFragment(arrow2); + interactions.AddFragment(arrow3); + + // Act + var result = arrow3.StatementsBeforeSelf(); + + // Assert + result.Count.ShouldBe(2); + result[0].ShouldBe(arrow1); + result[1].ShouldBe(arrow2); + } + + [TestMethod] + public void Descendants_MixedFragmentTypes_Should_FilterCorrectly() + { + // Arrange + var interactions = new Interactions(); + var alt = new Alt(); + var section = new AltSection { GroupType = "alt" }; + var arrow1 = new Arrow { Source = "A", Target = "B" }; + var arrow2 = new Arrow { Source = "B", Target = "C" }; + + section.AddFragment(arrow1); + alt.AddSection(section); + + var nodes = new InteractionFragment[] { alt, arrow2 }; + + // Act + var arrows = nodes.Descendants(); + var alts = nodes.Descendants(); + + // Assert + arrows.Count.ShouldBe(2); + arrows.ShouldContain(arrow1); + arrows.ShouldContain(arrow2); + + alts.ShouldHaveSingleItem(); + alts[0].ShouldBe(alt); + } +} diff --git a/tests/DendroDocs.Client.Tests/Uml/Fragments/InteractionFragmentTests.cs b/tests/DendroDocs.Client.Tests/Uml/Fragments/InteractionFragmentTests.cs new file mode 100644 index 0000000..c362aac --- /dev/null +++ b/tests/DendroDocs.Client.Tests/Uml/Fragments/InteractionFragmentTests.cs @@ -0,0 +1,151 @@ +namespace DendroDocs.Uml.Fragments.Tests; + +[TestClass] +public class InteractionFragmentTests +{ + [TestMethod] + public void AddFragment_ValidFragment_Should_AddToFragments() + { + // Arrange + var parent = new TestInteractionFragment(); + var child = new TestInteractionFragment(); + + // Act + parent.AddFragment(child); + + // Assert + parent.Fragments.ShouldHaveSingleItem(); + parent.Fragments[0].ShouldBe(child); + child.Parent.ShouldBe(parent); + } + + [TestMethod] + public void AddFragment_NullFragment_Should_Throw() + { + // Arrange + var parent = new TestInteractionFragment(); + + // Act + Action action = () => parent.AddFragment(null!); + + // Assert + action.ShouldThrow(); + } + + [TestMethod] + public void AddFragments_ValidFragments_Should_AddAllToFragments() + { + // Arrange + var parent = new TestInteractionFragment(); + var child1 = new TestInteractionFragment(); + var child2 = new TestInteractionFragment(); + var children = new[] { child1, child2 }; + + // Act + parent.AddFragments(children); + + // Assert + parent.Fragments.Count.ShouldBe(2); + parent.Fragments[0].ShouldBe(child1); + parent.Fragments[1].ShouldBe(child2); + child1.Parent.ShouldBe(parent); + child2.Parent.ShouldBe(parent); + } + + [TestMethod] + public void AddFragments_NullFragments_Should_Throw() + { + // Arrange + var parent = new TestInteractionFragment(); + + // Act + Action action = () => parent.AddFragments(null!); + + // Assert + action.ShouldThrow(); + } + + [TestMethod] + public void AddFragments_EmptyCollection_Should_NotThrow() + { + // Arrange + var parent = new TestInteractionFragment(); + + // Act + Action action = () => parent.AddFragments(Array.Empty()); + + // Assert + action.ShouldNotThrow(); + parent.Fragments.ShouldBeEmpty(); + } + + [TestMethod] + public void Parent_SetAndGet_Should_WorkCorrectly() + { + // Arrange + var parent = new TestInteractionFragment(); + var child = new TestInteractionFragment(); + + // Act + child.Parent = parent; + + // Assert + child.Parent.ShouldBe(parent); + } + + [TestMethod] + public void Fragments_InitialState_Should_BeEmpty() + { + // Arrange & Act + var fragment = new TestInteractionFragment(); + + // Assert + fragment.Fragments.ShouldNotBeNull(); + fragment.Fragments.ShouldBeEmpty(); + } + + [TestMethod] + public void AddFragment_MultipleFragments_Should_MaintainOrder() + { + // Arrange + var parent = new TestInteractionFragment(); + var child1 = new TestInteractionFragment(); + var child2 = new TestInteractionFragment(); + var child3 = new TestInteractionFragment(); + + // Act + parent.AddFragment(child1); + parent.AddFragment(child2); + parent.AddFragment(child3); + + // Assert + parent.Fragments.Count.ShouldBe(3); + parent.Fragments[0].ShouldBe(child1); + parent.Fragments[1].ShouldBe(child2); + parent.Fragments[2].ShouldBe(child3); + } + + [TestMethod] + public void AddFragment_ReparentingFragment_Should_UpdateParent() + { + // Arrange + var parent1 = new TestInteractionFragment(); + var parent2 = new TestInteractionFragment(); + var child = new TestInteractionFragment(); + + parent1.AddFragment(child); + + // Act + parent2.AddFragment(child); + + // Assert + child.Parent.ShouldBe(parent2); + parent1.Fragments.ShouldHaveSingleItem(); + parent2.Fragments.ShouldHaveSingleItem(); + } + + // Helper class for testing the abstract InteractionFragment + private class TestInteractionFragment : InteractionFragment + { + } +} \ No newline at end of file diff --git a/tests/DendroDocs.Client.Tests/Uml/Fragments/InteractionsTests.cs b/tests/DendroDocs.Client.Tests/Uml/Fragments/InteractionsTests.cs new file mode 100644 index 0000000..4497b61 --- /dev/null +++ b/tests/DendroDocs.Client.Tests/Uml/Fragments/InteractionsTests.cs @@ -0,0 +1,124 @@ +namespace DendroDocs.Uml.Fragments.Tests; + +[TestClass] +public class InteractionsTests +{ + [TestMethod] + public void Constructor_Should_InitializeAsInteractionFragment() + { + // Act + var interactions = new Interactions(); + + // Assert + interactions.Fragments.ShouldNotBeNull(); + interactions.Fragments.ShouldBeEmpty(); + interactions.Parent.ShouldBeNull(); + } + + [TestMethod] + public void AddFragment_Arrow_Should_AddToFragments() + { + // Arrange + var interactions = new Interactions(); + var arrow = new Arrow { Source = "A", Target = "B", Name = "message" }; + + // Act + interactions.AddFragment(arrow); + + // Assert + interactions.Fragments.ShouldHaveSingleItem(); + interactions.Fragments[0].ShouldBe(arrow); + arrow.Parent.ShouldBe(interactions); + } + + [TestMethod] + public void AddFragment_Alt_Should_AddToFragments() + { + // Arrange + var interactions = new Interactions(); + var alt = new Alt(); + var section = new AltSection { GroupType = "alt", Label = "condition" }; + alt.AddSection(section); + + // Act + interactions.AddFragment(alt); + + // Assert + interactions.Fragments.ShouldHaveSingleItem(); + interactions.Fragments[0].ShouldBe(alt); + alt.Parent.ShouldBe(interactions); + } + + [TestMethod] + public void AddFragment_NestedInteractions_Should_CreateHierarchy() + { + // Arrange + var parentInteractions = new Interactions(); + var childInteractions = new Interactions(); + var arrow = new Arrow { Source = "A", Target = "B" }; + + // Act + childInteractions.AddFragment(arrow); + parentInteractions.AddFragment(childInteractions); + + // Assert + parentInteractions.Fragments.ShouldHaveSingleItem(); + parentInteractions.Fragments[0].ShouldBe(childInteractions); + childInteractions.Parent.ShouldBe(parentInteractions); + childInteractions.Fragments.ShouldHaveSingleItem(); + arrow.Parent.ShouldBe(childInteractions); + } + + [TestMethod] + public void InheritsFromInteractionFragment_Should_SupportAllFragmentOperations() + { + // Arrange + var interactions = new Interactions(); + var arrow1 = new Arrow { Source = "A", Target = "B", Name = "first" }; + var arrow2 = new Arrow { Source = "B", Target = "C", Name = "second" }; + var fragments = new InteractionFragment[] { arrow1, arrow2 }; + + // Act + interactions.AddFragments(fragments); + + // Assert + interactions.Fragments.Count.ShouldBe(2); + interactions.Fragments[0].ShouldBe(arrow1); + interactions.Fragments[1].ShouldBe(arrow2); + arrow1.Parent.ShouldBe(interactions); + arrow2.Parent.ShouldBe(interactions); + } + + [TestMethod] + public void CanContainMixedFragmentTypes() + { + // Arrange + var interactions = new Interactions(); + var arrow = new Arrow { Source = "A", Target = "B" }; + var alt = new Alt(); + var section = new AltSection { GroupType = "alt" }; + alt.AddSection(section); + var nestedInteractions = new Interactions(); + + // Act + interactions.AddFragment(arrow); + interactions.AddFragment(alt); + interactions.AddFragment(nestedInteractions); + + // Assert + interactions.Fragments.Count.ShouldBe(3); + interactions.Fragments[0].ShouldBeOfType(); + interactions.Fragments[1].ShouldBeOfType(); + interactions.Fragments[2].ShouldBeOfType(); + } + + [TestMethod] + public void EmptyInteractions_Should_HaveNoFragments() + { + // Arrange & Act + var interactions = new Interactions(); + + // Assert + interactions.Fragments.ShouldBeEmpty(); + } +} diff --git a/tests/DendroDocs.Client.Tests/packages.lock.json b/tests/DendroDocs.Client.Tests/packages.lock.json index d553caf..4ac2962 100644 --- a/tests/DendroDocs.Client.Tests/packages.lock.json +++ b/tests/DendroDocs.Client.Tests/packages.lock.json @@ -244,14 +244,15 @@ "dendrodocs.client": { "type": "Project", "dependencies": { - "DendroDocs.Shared": "[0.3.2, )" + "DendroDocs.Shared": "[0.4.2, )", + "PlantUml.Builder": "[0.1.63, )" } }, "DendroDocs.Shared": { "type": "CentralTransitive", - "requested": "[0.3.2, )", - "resolved": "0.3.2", - "contentHash": "8k305PV9XQwZle+1EXH1ckGc/QJRYq/6eQOz2W6EFM0arobYLbcub1aCKiW6nZ7LGXdwEsB/WOEJjZ+sd6j+9w==", + "requested": "[0.4.2, )", + "resolved": "0.4.2", + "contentHash": "YfYo/kboAwdy8Qo0Iu1FF7+pZ+Fd8BHIyM8XbHTczjYjcmVsSJl2VS1oCJTj5eymMHqWMX2eTzQWRMByxnVVaA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } @@ -261,6 +262,12 @@ "requested": "[13.0.3, )", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "PlantUml.Builder": { + "type": "CentralTransitive", + "requested": "[0.1.63, )", + "resolved": "0.1.63", + "contentHash": "oidQGXTbe3E9SQ3UWLtUglJNgGEg2hWyCc4vDU1xYtESG9hZoO93tRu2QIhH/KCGSBh50hMuOMRXBUHoIyiJbQ==" } }, "net8.0/linux-x64": {