diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index 3da0a0e1b4..f5fc4e0867 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -199,7 +197,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase int ParentId { get; set; } + /// + /// Gets or sets the sort order of the entity. + /// + int SortOrder { get; set; } + /// /// Gets or sets a value indicating whether this entity is in the recycle bin. /// diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs index 9edf00d6fb..5e8e412116 100644 --- a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -1,30 +1,57 @@ +using System.Collections.Concurrent; + namespace Umbraco.Cms.Core.Models.Navigation; public sealed class NavigationNode { - private List _children; + private HashSet _children; public Guid Key { get; private set; } - public NavigationNode? Parent { get; private set; } + public int SortOrder { get; private set; } - public IEnumerable Children => _children.AsEnumerable(); + public Guid? Parent { get; private set; } - public NavigationNode(Guid key) + public ISet Children => _children; + + public NavigationNode(Guid key, int sortOrder = 0) { Key = key; - _children = new List(); + SortOrder = sortOrder; + _children = new HashSet(); } - public void AddChild(NavigationNode child) + public void UpdateSortOrder(int newSortOrder) => SortOrder = newSortOrder; + + public void AddChild(ConcurrentDictionary navigationStructure, Guid childKey) { - child.Parent = this; - _children.Add(child); + if (navigationStructure.TryGetValue(childKey, out NavigationNode? child) is false) + { + throw new KeyNotFoundException($"Item with key '{childKey}' was not found in the navigation structure."); + } + + child.Parent = Key; + + // Add it as the last item + child.SortOrder = _children.Count; + + _children.Add(childKey); + + // Update the navigation structure + navigationStructure[childKey] = child; } - public void RemoveChild(NavigationNode child) + public void RemoveChild(ConcurrentDictionary navigationStructure, Guid childKey) { - _children.Remove(child); + if (navigationStructure.TryGetValue(childKey, out NavigationNode? child) is false) + { + throw new KeyNotFoundException($"Item with key '{childKey}' was not found in the navigation structure."); + } + + _children.Remove(childKey); child.Parent = null; + + // Update the navigation structure + navigationStructure[childKey] = child; } } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index a2cd8ea354..10b2b6ba1c 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using Umbraco.Cms.Core.Factories; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Navigation; using Umbraco.Cms.Core.Persistence.Repositories; @@ -97,11 +96,13 @@ internal abstract class ContentNavigationServiceBase // Recursively remove all descendants and add them to recycle bin AddDescendantsToRecycleBinRecursively(nodeToRemove); + // Reset the SortOrder based on its new position in the bin + nodeToRemove.UpdateSortOrder(_recycleBinNavigationStructure.Count); return _recycleBinNavigationStructure.TryAdd(nodeToRemove.Key, nodeToRemove) && _navigationStructure.TryRemove(key, out _); } - public bool Add(Guid key, Guid? parentKey = null) + public bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null) { NavigationNode? parentNode = null; if (parentKey.HasValue) @@ -116,13 +117,14 @@ internal abstract class ContentNavigationServiceBase _roots.Add(key); } - var newNode = new NavigationNode(key); + // Note: sortOrder can't be automatically determined for items at root level, so it needs to be passed in + var newNode = new NavigationNode(key, sortOrder ?? 0); if (_navigationStructure.TryAdd(key, newNode) is false) { return false; // Node with this key already exists } - parentNode?.AddChild(newNode); + parentNode?.AddChild(_navigationStructure, key); return true; } @@ -155,13 +157,25 @@ internal abstract class ContentNavigationServiceBase } // Remove the node from its current parent's children list - if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Key, out var currentParentNode)) + if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Value, out NavigationNode? currentParentNode)) { - currentParentNode.RemoveChild(nodeToMove); + currentParentNode.RemoveChild(_navigationStructure, key); } // Set the new parent for the node (if parent node is null - the node is moved to root) - targetParentNode?.AddChild(nodeToMove); + targetParentNode?.AddChild(_navigationStructure, key); + + return true; + } + + public bool UpdateSortOrder(Guid key, int newSortOrder) + { + if (_navigationStructure.TryGetValue(key, out NavigationNode? node) is false) + { + return false; // Node doesn't exist + } + + node.UpdateSortOrder(newSortOrder); return true; } @@ -195,8 +209,7 @@ internal abstract class ContentNavigationServiceBase } // Set the new parent for the node (if parent node is null - the node is moved to root) - targetParentNode?.AddChild(nodeToRestore); - + targetParentNode?.AddChild(_recycleBinNavigationStructure, key); // Restore the node and its descendants from the recycle bin to the main structure RestoreNodeAndDescendantsRecursively(nodeToRestore); @@ -240,7 +253,7 @@ internal abstract class ContentNavigationServiceBase { if (structure.TryGetValue(childKey, out NavigationNode? childNode)) { - parentKey = childNode.Parent?.Key; + parentKey = childNode.Parent; return true; } @@ -252,7 +265,10 @@ internal abstract class ContentNavigationServiceBase private bool TryGetRootKeysFromStructure(IList input, out IEnumerable rootKeys) { // TODO can we make this more efficient? - rootKeys = input.ToArray(); + // Sort by SortOrder + rootKeys = input + .OrderBy(key => _navigationStructure[key].SortOrder) + .ToList(); return true; } @@ -266,7 +282,9 @@ internal abstract class ContentNavigationServiceBase return false; } - childrenKeys = parentNode.Children.Select(child => child.Key); + // Keep children keys ordered based on their SortOrder + childrenKeys = GetOrderedChildren(parentNode, structure).ToList(); + return true; } @@ -281,7 +299,7 @@ internal abstract class ContentNavigationServiceBase return false; } - GetDescendantsRecursively(parentNode, descendants); + GetDescendantsRecursively(structure, parentNode, descendants); descendantsKeys = descendants; return true; @@ -291,17 +309,16 @@ internal abstract class ContentNavigationServiceBase { var ancestors = new List(); - if (structure.TryGetValue(childKey, out NavigationNode? childNode) is false) + if (structure.TryGetValue(childKey, out NavigationNode? node) is false) { // Child doesn't exist ancestorsKeys = []; return false; } - while (childNode?.Parent is not null) + while (node.Parent is not null && structure.TryGetValue(node.Parent.Value, out node)) { - ancestors.Add(childNode.Parent.Key); - childNode = childNode.Parent; + ancestors.Add(node.Key); } ancestorsKeys = ancestors; @@ -322,12 +339,13 @@ internal abstract class ContentNavigationServiceBase // To find siblings of a node at root level, we need to iterate over all items and add those with null Parent siblingsKeys = structure .Where(kv => kv.Value.Parent is null && kv.Key != key) + .OrderBy(kv => kv.Value.SortOrder) .Select(kv => kv.Key) .ToList(); return true; } - if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys) is false) + if (TryGetChildrenKeys(node.Parent.Value, out IEnumerable childrenKeys) is false) { return false; // Couldn't retrieve children keys } @@ -337,12 +355,18 @@ internal abstract class ContentNavigationServiceBase return true; } - private void GetDescendantsRecursively(NavigationNode node, List descendants) + private void GetDescendantsRecursively(ConcurrentDictionary structure, NavigationNode node, List descendants) { - foreach (NavigationNode child in node.Children) + var childrenKeys = GetOrderedChildren(node, structure).ToList(); + foreach (Guid childKey in childrenKeys) { - descendants.Add(child.Key); - GetDescendantsRecursively(child, descendants); + descendants.Add(childKey); + + // Retrieve the child node and its descendants + if (structure.TryGetValue(childKey, out NavigationNode? childNode)) + { + GetDescendantsRecursively(structure, childNode, descendants); + } } } @@ -354,9 +378,9 @@ internal abstract class ContentNavigationServiceBase } // Remove the node from its parent's children list - if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode)) + if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Value, out NavigationNode? parentNode)) { - parentNode.RemoveChild(nodeToRemove); + parentNode.RemoveChild(structure, key); } return true; @@ -366,25 +390,39 @@ internal abstract class ContentNavigationServiceBase { _recycleBinRoots.Add(node.Key); _roots.Remove(node.Key); + var childrenKeys = GetOrderedChildren(node, _navigationStructure).ToList(); - foreach (NavigationNode child in node.Children) + foreach (Guid childKey in childrenKeys) { - AddDescendantsToRecycleBinRecursively(child); + if (_navigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false) + { + continue; + } + + // Reset the SortOrder based on its new position in the bin + childNode.UpdateSortOrder(_recycleBinNavigationStructure.Count); + AddDescendantsToRecycleBinRecursively(childNode); // Only remove the child from the main structure if it was successfully added to the recycle bin - if (_recycleBinNavigationStructure.TryAdd(child.Key, child)) + if (_recycleBinNavigationStructure.TryAdd(childKey, childNode)) { - _navigationStructure.TryRemove(child.Key, out _); + _navigationStructure.TryRemove(childKey, out _); } } } private void RemoveDescendantsRecursively(NavigationNode node) { - foreach (NavigationNode child in node.Children) + var childrenKeys = GetOrderedChildren(node, _recycleBinNavigationStructure).ToList(); + foreach (Guid childKey in childrenKeys) { - RemoveDescendantsRecursively(child); - _recycleBinNavigationStructure.TryRemove(child.Key, out _); + if (_recycleBinNavigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false) + { + continue; + } + + RemoveDescendantsRecursively(childNode); + _recycleBinNavigationStructure.TryRemove(childKey, out _); } } @@ -394,28 +432,41 @@ internal abstract class ContentNavigationServiceBase { _roots.Add(node.Key); } - _recycleBinRoots.Remove(node.Key); - foreach (NavigationNode child in node.Children) + _recycleBinRoots.Remove(node.Key); + var childrenKeys = GetOrderedChildren(node, _recycleBinNavigationStructure).ToList(); + + foreach (Guid childKey in childrenKeys) { - RestoreNodeAndDescendantsRecursively(child); + if (_recycleBinNavigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false) + { + continue; + } + + RestoreNodeAndDescendantsRecursively(childNode); // Only remove the child from the recycle bin structure if it was successfully added to the main one - if (_navigationStructure.TryAdd(child.Key, child)) + if (_navigationStructure.TryAdd(childKey, childNode)) { - _recycleBinNavigationStructure.TryRemove(child.Key, out _); + _recycleBinNavigationStructure.TryRemove(childKey, out _); } } } + private IEnumerable GetOrderedChildren(NavigationNode node, ConcurrentDictionary structure) + => node.Children + .Where(structure.ContainsKey) + .OrderBy(childKey => structure[childKey].SortOrder) + .ToList(); + private static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, IList roots, IEnumerable entities) { var entityList = entities.ToList(); - IDictionary idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); + var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); foreach (INavigationModel entity in entityList) { - var node = new NavigationNode(entity.Key); + var node = new NavigationNode(entity.Key, entity.SortOrder); nodesStructure[entity.Key] = node; // We don't set the parent for items under root, it will stay null @@ -433,7 +484,7 @@ internal abstract class ContentNavigationServiceBase // If the parent node exists in the nodesStructure, add the node to the parent's children (parent is set as well) if (nodesStructure.TryGetValue(parentKey, out NavigationNode? parentNode)) { - parentNode.AddChild(node); + parentNode.AddChild(nodesStructure, entity.Key); } } } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index 4ab8458f18..80dce527e1 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -33,11 +33,20 @@ public interface INavigationManagementService /// The unique identifier of the parent node. If null, the new node will be added to /// the root level. /// + /// + /// Optional value to define the node's position among its siblings when + /// adding node at root level. /// /// true if the node was successfully added to the main navigation structure; /// otherwise, false. /// - bool Add(Guid key, Guid? parentKey = null); + /// + /// The sort order is particularly important when adding nodes at the root level. For child nodes, + /// it can usually be determined by the number of existing children under the parent. However, + /// when adding nodes directly to the root (where parentKey is null), a sort order must be provided + /// to ensure the item appears in the correct position among other root-level items. + /// + bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null); /// /// Moves an existing node to a new parent in the main navigation structure. If a @@ -54,4 +63,15 @@ public interface INavigationManagementService /// in the main navigation structure; otherwise, false. /// bool Move(Guid key, Guid? targetParentKey = null); + + /// + /// Updates the sort order of a node in the main navigation structure. + /// The sort order of other nodes in the same level will be adjusted accordingly. + /// + /// The unique identifier of the node to update. + /// The new sort order for the node. + /// + /// true if the node's sort order was successfully updated; otherwise, false. + /// + bool UpdateSortOrder(Guid key, int newSortOrder); } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index e440c30794..7977e0c5db 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -18,7 +18,7 @@ public interface INavigationQueryService bool TryGetDescendantsKeysOrSelfKeys(Guid childKey, out IEnumerable descendantsOrSelfKeys) { - if(TryGetDescendantsKeys(childKey, out var descendantsKeys)) + if (TryGetDescendantsKeys(childKey, out IEnumerable? descendantsKeys)) { descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys); return true; @@ -28,14 +28,13 @@ public interface INavigationQueryService return false; } - bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); bool TryGetAncestorsOrSelfKeys(Guid childKey, out IEnumerable ancestorsOrSelfKeys) { - if(TryGetAncestorsKeys(childKey, out var ancestorsKeys)) + if (TryGetAncestorsKeys(childKey, out IEnumerable? ancestorsKeys)) { - ancestorsOrSelfKeys = childKey.Yield().Concat(ancestorsKeys); + ancestorsOrSelfKeys = childKey.Yield().Concat(ancestorsKeys); return true; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs index 156a85b19c..6b452b0c8a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs @@ -19,6 +19,10 @@ internal class NavigationDto : INavigationModel [Column(NodeDto.ParentIdColumnName)] public int ParentId { get; set; } + /// + [Column(NodeDto.SortOrderColumnName)] + public int SortOrder { get; set; } + /// [Column(NodeDto.TrashedColumnName)] public bool Trashed { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index 2ac62429ba..c136f45fd4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -17,6 +17,7 @@ public class NodeDto public const string IdColumnName = "id"; public const string KeyColumnName = "uniqueId"; public const string ParentIdColumnName = "parentId"; + public const string SortOrderColumnName = "sortOrder"; public const string TrashedColumnName = "trashed"; private int? _userId; @@ -46,7 +47,7 @@ public class NodeDto [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")] public string Path { get; set; } = null!; - [Column("sortOrder")] + [Column(SortOrderColumnName)] [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType_trashed_sorted", ForColumns = "nodeObjectType,trashed,sortOrder,id", IncludeColumns = "uniqueID,parentID,level,path,nodeUser,text,createDate")] public int SortOrder { get; set; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs index 95659b38be..08327d200a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs @@ -106,4 +106,34 @@ public partial class DocumentNavigationServiceTests Assert.AreNotEqual(GreatGrandchild1.Key, copiedGreatGrandChild1.Key); }); } + + [Test] + [TestCase(null)] // Content root + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public async Task Copying_Content_Adds_It_Last(Guid? parentKey) + { + // Act + var copyAttempt = await ContentEditingService.CopyAsync(Grandchild1.Key, parentKey, false, true, Constants.Security.SuperUserKey); + Guid copiedItemKey = copyAttempt.Result.Key; + + // Assert + if (parentKey is null) + { + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + Assert.AreEqual(copiedItemKey, rootKeys.Last()); + } + else + { + DocumentNavigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable childrenKeys); + Assert.AreEqual(copiedItemKey, childrenKeys.Last()); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs index 4138f80b07..3e0d3e85f5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.ContentEditing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -37,7 +36,7 @@ public partial class DocumentNavigationServiceTests // Arrange DocumentNavigationQueryService.TryGetChildrenKeys(Child1.Key, out IEnumerable initialChildrenKeys); var initialChild1ChildrenCount = initialChildrenKeys.Count(); - var createModel = CreateContentCreateModel("Child1Child", Guid.NewGuid(), Child1.Key); + var createModel = CreateContentCreateModel("Grandchild 3", Guid.NewGuid(), Child1.Key); // Act var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); @@ -55,4 +54,37 @@ public partial class DocumentNavigationServiceTests Assert.IsTrue(childrenList.Contains(createdItemKey)); }); } + + [Test] + [TestCase(null)] // Content root + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public async Task Creating_Child_Content_Adds_It_As_The_Last_Child(Guid? parentKey) + { + // Arrange + Guid newNodeKey = Guid.NewGuid(); + var createModel = CreateContentCreateModel("Child", newNodeKey, parentKey); + + // Act + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + // Assert + if (parentKey is null) + { + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + Assert.AreEqual(newNodeKey, rootKeys.Last()); + } + else + { + DocumentNavigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable childrenKeys); + Assert.AreEqual(newNodeKey, childrenKeys.Last()); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs index 8a60eaecc8..32ba7934b1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs @@ -39,4 +39,42 @@ public partial class DocumentNavigationServiceTests } }); } + + // TODO: Add more test cases + [Test] + public async Task Sort_Order_Of_Siblings_Updates_When_Deleting_Content_And_Adding_New_One() + { + // Arrange + Guid nodeToDelete = Child3.Key; + Guid node = Child1.Key; + + // Act + await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion); + var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count); + Assert.AreEqual(Child2.Key, siblingsKeysAfterDeletionList[0]); + }); + + // Create a new sibling under the same parent + var key = Guid.NewGuid(); + var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); + var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList(); + + // Verify sibling order after creating the new content + Assert.Multiple(() => + { + Assert.AreEqual(2, siblingsKeysAfterCreationList.Count); + Assert.AreEqual(Child2.Key, siblingsKeysAfterCreationList[0]); + Assert.AreEqual(key, siblingsKeysAfterCreationList[1]); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs index f31f4f7907..0c8dc8c502 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs @@ -94,4 +94,72 @@ public partial class DocumentNavigationServiceTests Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); }); } + + [Test] + [TestCase(null)] // Content root + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public async Task Moving_Content_Adds_It_Last(Guid? targetParentKey) + { + // Arrange + Guid nodeToMove = Grandchild1.Key; + + // Act + await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Assert + if (targetParentKey is null) + { + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + Assert.AreEqual(nodeToMove, rootKeys.Last()); + } + else + { + DocumentNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys); + Assert.AreEqual(nodeToMove, childrenKeys.Last()); + } + } + + // TODO: Add more test cases + [Test] + public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Content_And_Adding_New_One() + { + // Arrange + Guid nodeToMove = Child3.Key; + Guid node = Child1.Key; + + // Act + await ContentEditingService.MoveAsync(nodeToMove, null, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterMoving); + var siblingsKeysAfterMovingList = siblingsKeysAfterMoving.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, siblingsKeysAfterMovingList.Count); + Assert.AreEqual(Child2.Key, siblingsKeysAfterMovingList[0]); + }); + + // Create a new sibling under the same parent + var key = Guid.NewGuid(); + var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); + var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList(); + + // Verify sibling order after creating the new content + Assert.Multiple(() => + { + Assert.AreEqual(2, siblingsKeysAfterCreationList.Count); + Assert.AreEqual(Child2.Key, siblingsKeysAfterCreationList[0]); + Assert.AreEqual(key, siblingsKeysAfterCreationList[1]); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs index 9e5e7dcc69..f14c83d8fc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class DocumentNavigationServiceTests { [Test] - public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() + public async Task Parent_And_Descendants_Are_Updated_When_Content_Is_Moved_To_Recycle_Bin() { // Arrange Guid nodeToMoveToRecycleBin = Child3.Key; @@ -19,7 +19,7 @@ public partial class DocumentNavigationServiceTests DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToMoveToRecycleBin, out IEnumerable initialDescendantsKeys); var beforeMoveDescendants = initialDescendantsKeys.ToList(); DocumentNavigationQueryService.TryGetChildrenKeys(originalParentKey.Value, out IEnumerable initialParentChildrenKeys); - var beforeMoveParentSiblingsCount = initialParentChildrenKeys.Count(); + var beforeMoveParentChildrenCount = initialParentChildrenKeys.Count(); // Act await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); @@ -30,7 +30,7 @@ public partial class DocumentNavigationServiceTests DocumentNavigationQueryService.TryGetDescendantsKeysInBin(nodeToMoveToRecycleBin, out IEnumerable afterMoveDescendantsKeys); var afterMoveDescendants = afterMoveDescendantsKeys.ToList(); DocumentNavigationQueryService.TryGetChildrenKeys((Guid)originalParentKey, out IEnumerable afterMoveParentChildrenKeys); - var afterMoveParentSiblingsCount = afterMoveParentChildrenKeys.Count(); + var afterMoveParentChildrenCount = afterMoveParentChildrenKeys.Count(); DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable afterMoveRecycleBinSiblingsKeys); var afterMoveRecycleBinSiblingsCount = afterMoveRecycleBinSiblingsKeys.Count(); @@ -42,8 +42,78 @@ public partial class DocumentNavigationServiceTests Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null) Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); - Assert.AreEqual(beforeMoveParentSiblingsCount - 1, afterMoveParentSiblingsCount); + Assert.AreEqual(beforeMoveParentChildrenCount - 1, afterMoveParentChildrenCount); Assert.AreEqual(beforeMoveRecycleBinSiblingsCount + 1, afterMoveRecycleBinSiblingsCount); }); } + + // TODO: Add more test cases + [Test] + public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Content_To_Recycle_Bin_And_Adding_New_One() + { + // Arrange + Guid nodeToMoveToRecycleBin = Child3.Key; + Guid node = Child1.Key; + + // Act + await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion); + var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count); + Assert.AreEqual(Child2.Key, siblingsKeysAfterDeletionList[0]); + }); + + // Create a new sibling under the same parent + var key = Guid.NewGuid(); + var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); + var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList(); + + // Verify sibling order after creating the new content + Assert.Multiple(() => + { + Assert.AreEqual(2, siblingsKeysAfterCreationList.Count); + Assert.AreEqual(Child2.Key, siblingsKeysAfterCreationList[0]); + Assert.AreEqual(key, siblingsKeysAfterCreationList[1]); + }); + } + + [Test] + public async Task Sort_Order_Of_Chilldren_Is_Maintained_When_Moving_Content_To_Recycle_Bin() + { + // Arrange + Guid nodeToMoveToRecycleBin = Child1.Key; + + // Create a new grandchild under Child1 + var key = Guid.NewGuid(); + var createModel = CreateContentCreateModel("Grandchild 3", key, nodeToMoveToRecycleBin); + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + DocumentNavigationQueryService.TryGetChildrenKeys(nodeToMoveToRecycleBin, out IEnumerable childrenKeysBeforeDeletion); + var childrenKeysBeforeDeletionList = childrenKeysBeforeDeletion.ToList(); + + // Act + await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetChildrenKeysInBin(nodeToMoveToRecycleBin, out IEnumerable childrenKeysAfterDeletion); + var childrenKeysAfterDeletionList = childrenKeysAfterDeletion.ToList(); + + // Verify children order in the bin + Assert.Multiple(() => + { + Assert.AreEqual(3, childrenKeysAfterDeletionList.Count); + Assert.AreEqual(Grandchild1.Key, childrenKeysAfterDeletionList[0]); + Assert.AreEqual(Grandchild2.Key, childrenKeysAfterDeletionList[1]); + Assert.AreEqual(key, childrenKeysAfterDeletionList[2]); + Assert.IsTrue(childrenKeysBeforeDeletionList.SequenceEqual(childrenKeysAfterDeletionList)); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs index e57f0c652c..c4a6a2a3de 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs @@ -50,4 +50,37 @@ public partial class DocumentNavigationServiceTests Assert.AreEqual(targetParentKey, restoredItemParentKey); }); } + + [Test] + [TestCase(null)] // Content root + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public async Task Restoring_Content_Adds_It_Last(Guid? targetParentKey) + { + // Arrange + Guid nodeToRestore = Child3.Key; + + // Move node to recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); + + // Act + await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); + + // Assert + if (targetParentKey is null) + { + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + Assert.AreEqual(nodeToRestore, rootKeys.Last()); + } + else + { + DocumentNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys); + Assert.AreEqual(nodeToRestore, childrenKeys.Last()); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs new file mode 100644 index 0000000000..7d1e113974 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs @@ -0,0 +1,199 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Reversing_Children_Sort_Order() + { + // Arrange + Guid nodeToSortItsChildren = Root.Key; + DocumentNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable initialChildrenKeys); + List initialChildrenKeysList = initialChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(3, initialChildrenKeysList.Count); + + // Assert initial order + Assert.AreEqual(Child1.Key, initialChildrenKeysList[0]); + Assert.AreEqual(Child2.Key, initialChildrenKeysList[1]); + Assert.AreEqual(Child3.Key, initialChildrenKeysList[2]); + }); + + IEnumerable sortingModels = initialChildrenKeys + .Reverse() + .Select((key, index) => new SortingModel { Key = key, SortOrder = index }); + + // Act + await ContentEditingService.SortAsync(nodeToSortItsChildren, sortingModels, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable sortedChildrenKeys); + List sortedChildrenKeysList = sortedChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(Child3.Key, sortedChildrenKeysList[0]); + Assert.AreEqual(Child2.Key, sortedChildrenKeysList[1]); + Assert.AreEqual(Child1.Key, sortedChildrenKeysList[2]); + }); + + var expectedChildrenKeysList = initialChildrenKeys.Reverse().ToList(); + + // Check that the order matches what is expected + Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList)); + } + + [Test] + public async Task Structure_Updates_When_Children_Have_Custom_Sort_Order() + { + // Arrange + Guid node = Root.Key; + var customSortingModels = new List + { + new() { Key = Child2.Key, SortOrder = 0 }, // Move Child 2 to the position 1 + new() { Key = Child3.Key, SortOrder = 1 }, // Move Child 3 to the position 2 + new() { Key = Child1.Key, SortOrder = 2 }, // Move Child 1 to the position 3 + }; + + // Act + await ContentEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetChildrenKeys(node, out IEnumerable sortedChildrenKeys); + List sortedChildrenKeysList = sortedChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(Child2.Key, sortedChildrenKeysList[0]); + Assert.AreEqual(Child3.Key, sortedChildrenKeysList[1]); + Assert.AreEqual(Child1.Key, sortedChildrenKeysList[2]); + }); + + var expectedChildrenKeysList = customSortingModels + .OrderBy(x => x.SortOrder) + .Select(x => x.Key) + .ToList(); + + // Check that the order matches what is expected + Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList)); + } + + [Test] + public async Task Structure_Updates_When_Sorting_Items_At_Root() + { + // Arrange + var anotherRootCreateModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + await ContentEditingService.CreateAsync(anotherRootCreateModel, Constants.Security.SuperUserKey); + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); + + var sortingModels = initialRootKeys + .Reverse() + .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index }); + + // Act + await ContentEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable sortedRootKeys); + + var expectedRootKeysList = initialRootKeys.Reverse().ToList(); + + // Check that the order matches what is expected + Assert.IsTrue(expectedRootKeysList.SequenceEqual(sortedRootKeys)); + } + + [Test] + public async Task Descendants_Are_Returned_In_Correct_Order_After_Children_Are_Reordered() + { + // Arrange + Guid node = Root.Key; + DocumentNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable initialDescendantsKeys); + + var customSortingModels = new List + { + new() { Key = Child3.Key, SortOrder = 0 }, // Move Child 3 to the position 1 + new() { Key = Child1.Key, SortOrder = 1 }, // Move Child 1 to the position 2 + new() { Key = Child2.Key, SortOrder = 2 }, // Move Child 2 to the position 3 + }; + + var expectedDescendantsOrder = new List + { + Child3.Key, Grandchild4.Key, // Child 3 and its descendants + Child1.Key, Grandchild1.Key, Grandchild2.Key, // Child 1 and its descendants + Child2.Key, Grandchild3.Key, GreatGrandchild1.Key, // Child 2 and its descendants + }; + + // Act + await ContentEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable updatedDescendantsKeys); + List updatedDescendantsKeysList = updatedDescendantsKeys.ToList(); + + Assert.Multiple(() => + { + Assert.IsFalse(initialDescendantsKeys.SequenceEqual(updatedDescendantsKeysList)); + Assert.IsTrue(expectedDescendantsOrder.SequenceEqual(updatedDescendantsKeysList)); + }); + } + + [Test] + [TestCase(1, 2, 0, new[] { "B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE" })] // Custom sort order: Child 3, Child 1, Child 2; Expected order: Child 3, Child 1 + [TestCase(0, 1, 2, new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "B606E3FF-E070-4D46-8CB9-D31352029FDF" })] // Custom sort order: Child 1, Child 2, Child 3; Expected order: Child 1, Child 3 + [TestCase(2, 0, 1, new[] { "B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE" })] // Custom sort order: Child 2, Child 3, Child 1; Expected order: Child 3, Child 1 + public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting(int sortOrder1, int sortOrder2, int sortOrder3, string[] expectedSiblings) + { + // Arrange + Guid node = Child2.Key; + + var customSortingModels = new List + { + new() { Key = Child1.Key, SortOrder = sortOrder1 }, // Move Child 1 to the position sortOrder1 + new() { Key = Child2.Key, SortOrder = sortOrder2 }, // Move Child 2 to the position sortOrder2 + new() { Key = Child3.Key, SortOrder = sortOrder3 }, // Move Child 3 to the position sortOrder3 + }; + + Guid[] expectedSiblingsOrder = Array.ConvertAll(expectedSiblings, Guid.Parse); + + // Act + await ContentEditingService.SortAsync(Root.Key, customSortingModels, Constants.Security.SuperUserKey); // Using the parent key here + + // Assert + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys); + var sortedSiblingsKeysList = sortedSiblingsKeys.ToList(); + + Assert.IsTrue(expectedSiblingsOrder.SequenceEqual(sortedSiblingsKeysList)); + } + + [Test] + public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting_At_Root() + { + // Arrange + Guid node = Root.Key; + var anotherRootCreateModel1 = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + await ContentEditingService.CreateAsync(anotherRootCreateModel1, Constants.Security.SuperUserKey); + var anotherRootCreateModel2 = CreateContentCreateModel("Root 3", Guid.NewGuid(), Constants.System.RootKey); + await ContentEditingService.CreateAsync(anotherRootCreateModel2, Constants.Security.SuperUserKey); + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); + + var sortingModels = initialRootKeys + .Reverse() + .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index }); + + var expectedSiblingsKeysList = initialRootKeys.Reverse().Where(k => k != node).ToList(); + + // Act + await ContentEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys); + var sortedSiblingsKeysList = sortedSiblingsKeys.ToList(); + + Assert.IsTrue(expectedSiblingsKeysList.SequenceEqual(sortedSiblingsKeysList)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs index 3111e5c8e5..9b1baa3c71 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs @@ -11,7 +11,7 @@ public partial class MediaNavigationServiceTests // Arrange MediaNavigationQueryService.TryGetSiblingsKeys(Album.Key, out IEnumerable initialSiblingsKeys); var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); - var createModel = CreateMediaCreateModel("Root Image", Guid.NewGuid(), ImageMediaType.Key); + var createModel = CreateMediaCreateModel("Album 2", Guid.NewGuid(), FolderMediaType.Key); // Act var createAttempt = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); @@ -54,4 +54,32 @@ public partial class MediaNavigationServiceTests Assert.IsTrue(childrenList.Contains(createdItemKey)); }); } + + [Test] + [TestCase(null)] // Media root + [TestCase("1CD97C02-8534-4B72-AE9E-AE52EC94CF31")] // Album + [TestCase("139DC977-E50F-4382-9728-B278C4B7AC6A")] // Sub-album 1 + [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564")] // Sub-album 2 + [TestCase("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB")] // Sub-sub-album 1 + public async Task Creating_Child_Media_Adds_It_As_The_Last_Child(Guid? parentKey) + { + // Arrange + Guid newNodeKey = Guid.NewGuid(); + var createModel = CreateMediaCreateModel("Child Image", newNodeKey, ImageMediaType.Key, parentKey); + + // Act + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + // Assert + if (parentKey is null) + { + MediaNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + Assert.AreEqual(newNodeKey, rootKeys.Last()); + } + else + { + MediaNavigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable childrenKeys); + Assert.AreEqual(newNodeKey, childrenKeys.Last()); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs index 5eeadb2484..c728d6a6d3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs @@ -37,4 +37,42 @@ public partial class MediaNavigationServiceTests } }); } + + // TODO: Add more test cases + [Test] + public async Task Sort_Order_Of_Siblings_Updates_When_Deleting_Media_And_Adding_New_One() + { + // Arrange + Guid nodeToDelete = SubAlbum2.Key; + Guid node = Image1.Key; + + // Act + await MediaEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion); + var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count); + Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterDeletionList[0]); + }); + + // Create a new sibling under the same parent + var key = Guid.NewGuid(); + var createModel = CreateMediaCreateModel("Child Image", key, ImageMediaType.Key, Album.Key); + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); + var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList(); + + // Verify sibling order after creating the new media + Assert.Multiple(() => + { + Assert.AreEqual(2, siblingsKeysAfterCreationList.Count); + Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterCreationList[0]); + Assert.AreEqual(key, siblingsKeysAfterCreationList[1]); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs index 1677dcec45..a54455f630 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs @@ -94,4 +94,68 @@ public partial class MediaNavigationServiceTests Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); }); } + + [Test] + [TestCase(null)] // Media root + [TestCase("1CD97C02-8534-4B72-AE9E-AE52EC94CF31")] // Album + [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564")] // Sub-album 2 + [TestCase("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB")] // Sub-sub-album 1 + public async Task Moving_Media_Adds_It_Last(Guid? targetParentKey) + { + // Arrange + Guid nodeToMove = Image2.Key; + + // Act + await MediaEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Assert + if (targetParentKey is null) + { + MediaNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + Assert.AreEqual(nodeToMove, rootKeys.Last()); + } + else + { + MediaNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys); + Assert.AreEqual(nodeToMove, childrenKeys.Last()); + } + } + + // TODO: Add more test cases + [Test] + public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Media_And_Adding_New_One() + { + // Arrange + Guid nodeToMove = SubAlbum2.Key; + Guid node = Image1.Key; + + // Act + await MediaEditingService.MoveAsync(nodeToMove, null, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterMoving); + var siblingsKeysAfterMovingList = siblingsKeysAfterMoving.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, siblingsKeysAfterMovingList.Count); + Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterMovingList[0]); + }); + + // Create a new sibling under the same parent + var key = Guid.NewGuid(); + var createModel = CreateMediaCreateModel("Child Image", key, ImageMediaType.Key, Album.Key); + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); + var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList(); + + // Verify sibling order after creating the new media + Assert.Multiple(() => + { + Assert.AreEqual(2, siblingsKeysAfterCreationList.Count); + Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterMovingList[0]); + Assert.AreEqual(key, siblingsKeysAfterCreationList[1]); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs index f54eae0924..244e2be5dc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs @@ -45,4 +45,74 @@ public partial class MediaNavigationServiceTests Assert.AreEqual(beforeMoveRecycleBinSiblingsCount + 1, afterMoveRecycleBinSiblingsCount); }); } + + // TODO: Add more test cases + [Test] + public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Media_To_Recycle_Bin_And_Adding_New_One() + { + // Arrange + Guid nodeToMoveToRecycleBin = SubAlbum2.Key; + Guid node = Image1.Key; + + // Act + await MediaEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion); + var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count); + Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterDeletionList[0]); + }); + + // Create a new sibling under the same parent + var key = Guid.NewGuid(); + var createModel = CreateMediaCreateModel("Child Image", key, ImageMediaType.Key, Album.Key); + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); + var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList(); + + // Verify sibling order after creating the new media + Assert.Multiple(() => + { + Assert.AreEqual(2, siblingsKeysAfterCreationList.Count); + Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterDeletionList[0]); + Assert.AreEqual(key, siblingsKeysAfterCreationList[1]); + }); + } + + [Test] + public async Task Sort_Order_Of_Chilldren_Is_Maintained_When_Moving_Media_To_Recycle_Bin() + { + // Arrange + Guid nodeToMoveToRecycleBin = SubAlbum1.Key; + + // Create a new grandchild under Child1 + var key = Guid.NewGuid(); + var createModel = CreateMediaCreateModel("Image 5", key, ImageMediaType.Key, nodeToMoveToRecycleBin); + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + MediaNavigationQueryService.TryGetChildrenKeys(nodeToMoveToRecycleBin, out IEnumerable childrenKeysBeforeDeletion); + var childrenKeysBeforeDeletionList = childrenKeysBeforeDeletion.ToList(); + + // Act + await MediaEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetChildrenKeysInBin(nodeToMoveToRecycleBin, out IEnumerable childrenKeysAfterDeletion); + var childrenKeysAfterDeletionList = childrenKeysAfterDeletion.ToList(); + + // Verify children order in the bin + Assert.Multiple(() => + { + Assert.AreEqual(3, childrenKeysAfterDeletionList.Count); + Assert.AreEqual(Image2.Key, childrenKeysAfterDeletionList[0]); + Assert.AreEqual(Image3.Key, childrenKeysAfterDeletionList[1]); + Assert.AreEqual(key, childrenKeysAfterDeletionList[2]); + Assert.IsTrue(childrenKeysBeforeDeletionList.SequenceEqual(childrenKeysAfterDeletionList)); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs index 22e5e3d799..c0ff408125 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs @@ -50,4 +50,33 @@ public partial class MediaNavigationServiceTests Assert.AreEqual(targetParentKey, restoredItemParentKey); }); } + + [Test] + [TestCase(null)] // Media root + [TestCase("139DC977-E50F-4382-9728-B278C4B7AC6A")] // Sub-album 1 + [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564")] // Sub-album 2 + [TestCase("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB")] // Sub-sub-album 1 + public async Task Restoring_Content_Adds_It_Last(Guid? targetParentKey) + { + // Arrange + Guid nodeToRestore = Image1.Key; + + // Move node to recycle bin + await MediaEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); + + // Act + await MediaEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); + + // Assert + if (targetParentKey is null) + { + MediaNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); + Assert.AreEqual(nodeToRestore, rootKeys.Last()); + } + else + { + MediaNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys); + Assert.AreEqual(nodeToRestore, childrenKeys.Last()); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Sort.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Sort.cs new file mode 100644 index 0000000000..99f6e8f258 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Sort.cs @@ -0,0 +1,199 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Reversing_Children_Sort_Order() + { + // Arrange + Guid nodeToSortItsChildren = Album.Key; + MediaNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable initialChildrenKeys); + List initialChildrenKeysList = initialChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(3, initialChildrenKeysList.Count); + + // Assert initial order + Assert.AreEqual(Image1.Key, initialChildrenKeysList[0]); + Assert.AreEqual(SubAlbum1.Key, initialChildrenKeysList[1]); + Assert.AreEqual(SubAlbum2.Key, initialChildrenKeysList[2]); + }); + + IEnumerable sortingModels = initialChildrenKeys + .Reverse() + .Select((key, index) => new SortingModel { Key = key, SortOrder = index }); + + // Act + await MediaEditingService.SortAsync(nodeToSortItsChildren, sortingModels, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable sortedChildrenKeys); + List sortedChildrenKeysList = sortedChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(SubAlbum2.Key, sortedChildrenKeysList[0]); + Assert.AreEqual(SubAlbum1.Key, sortedChildrenKeysList[1]); + Assert.AreEqual(Image1.Key, sortedChildrenKeysList[2]); + }); + + var expectedChildrenKeysList = initialChildrenKeys.Reverse().ToList(); + + // Check that the order matches what is expected + Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList)); + } + + [Test] + public async Task Structure_Updates_When_Children_Have_Custom_Sort_Order() + { + // Arrange + Guid node = Album.Key; + var customSortingModels = new List + { + new() { Key = SubAlbum1.Key, SortOrder = 0 }, // Move Sub-album 1 to the position 1 + new() { Key = SubAlbum2.Key, SortOrder = 1 }, // Move Sub-album 2 to the position 2 + new() { Key = Image1.Key, SortOrder = 2 }, // Move Image 1 to the position 3 + }; + + // Act + await MediaEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetChildrenKeys(node, out IEnumerable sortedChildrenKeys); + List sortedChildrenKeysList = sortedChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(SubAlbum1.Key, sortedChildrenKeysList[0]); + Assert.AreEqual(SubAlbum2.Key, sortedChildrenKeysList[1]); + Assert.AreEqual(Image1.Key, sortedChildrenKeysList[2]); + }); + + var expectedChildrenKeysList = customSortingModels + .OrderBy(x => x.SortOrder) + .Select(x => x.Key) + .ToList(); + + // Check that the order matches what is expected + Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList)); + } + + [Test] + public async Task Structure_Updates_When_Sorting_Items_At_Root() + { + // Arrange + var anotherRootCreateModel = CreateMediaCreateModel("Album 2", Guid.NewGuid(), FolderMediaType.Key, Constants.System.RootKey); + await MediaEditingService.CreateAsync(anotherRootCreateModel, Constants.Security.SuperUserKey); + MediaNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); + + var sortingModels = initialRootKeys + .Reverse() + .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index }); + + // Act + await MediaEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetRootKeys(out IEnumerable sortedRootKeys); + + var expectedRootKeysList = initialRootKeys.Reverse().ToList(); + + // Check that the order matches what is expected + Assert.IsTrue(expectedRootKeysList.SequenceEqual(sortedRootKeys)); + } + + [Test] + public async Task Descendants_Are_Returned_In_Correct_Order_After_Children_Are_Reordered() + { + // Arrange + Guid node = Album.Key; + MediaNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable initialDescendantsKeys); + + var customSortingModels = new List + { + new() { Key = SubAlbum2.Key, SortOrder = 0 }, // Move Sub-album 2 to the position 1 + new() { Key = Image1.Key, SortOrder = 1 }, // Move Image 1 to the position 2 + new() { Key = SubAlbum1.Key, SortOrder = 2 }, // Move Sub-album 1 to the position 3 + }; + + var expectedDescendantsOrder = new List + { + SubAlbum2.Key, SubSubAlbum1.Key, Image4.Key, // Sub-album 2 and its descendants + Image1.Key, // Image 1 + SubAlbum1.Key, Image2.Key, Image3.Key, // Sub-album 1 and its descendants + }; + + // Act + await MediaEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable updatedDescendantsKeys); + List updatedDescendantsKeysList = updatedDescendantsKeys.ToList(); + + Assert.Multiple(() => + { + Assert.IsFalse(initialDescendantsKeys.SequenceEqual(updatedDescendantsKeysList)); + Assert.IsTrue(expectedDescendantsOrder.SequenceEqual(updatedDescendantsKeysList)); + }); + } + + [Test] + [TestCase(1, 2, 0, new[] { "DBCAFF2F-BFA4-4744-A948-C290C432D564", "03976EBE-A942-4F24-9885-9186E99AEF7C" })] // Custom sort order: Sub-album 2, Image 1, Sub-album 1; Expected order: Sub-album 2, Image 1 + [TestCase(0, 1, 2, new[] { "03976EBE-A942-4F24-9885-9186E99AEF7C", "DBCAFF2F-BFA4-4744-A948-C290C432D564" })] // Custom sort order: Image 1, Sub-album 1, Sub-album 2; Expected order: Image 1, Sub-album 2 + [TestCase(2, 0, 1, new[] { "DBCAFF2F-BFA4-4744-A948-C290C432D564", "03976EBE-A942-4F24-9885-9186E99AEF7C" })] // Custom sort order: Sub-album 1, Sub-album 2, Image 1; Expected order: Sub-album 2, Image 1 + public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting(int sortOrder1, int sortOrder2, int sortOrder3, string[] expectedSiblings) + { + // Arrange + Guid node = SubAlbum1.Key; + + var customSortingModels = new List + { + new() { Key = Image1.Key, SortOrder = sortOrder1 }, // Move Image 1 to the position sortOrder1 + new() { Key = SubAlbum1.Key, SortOrder = sortOrder2 }, // Move Sub-album 1 to the position sortOrder2 + new() { Key = SubAlbum2.Key, SortOrder = sortOrder3 }, // Move Sub-album 2 to the position sortOrder3 + }; + + Guid[] expectedSiblingsOrder = Array.ConvertAll(expectedSiblings, Guid.Parse); + + // Act + await MediaEditingService.SortAsync(Album.Key, customSortingModels, Constants.Security.SuperUserKey); // Using the parent key here + + // Assert + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys); + var sortedSiblingsKeysList = sortedSiblingsKeys.ToList(); + + Assert.IsTrue(expectedSiblingsOrder.SequenceEqual(sortedSiblingsKeysList)); + } + + [Test] + public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting_At_Root() + { + // Arrange + Guid node = Album.Key; + var anotherRootCreateModel1 = CreateMediaCreateModel("Album 2", Guid.NewGuid(), FolderMediaType.Key, Constants.System.RootKey); + await MediaEditingService.CreateAsync(anotherRootCreateModel1, Constants.Security.SuperUserKey); + var anotherRootCreateModel2 = CreateMediaCreateModel("Album 3", Guid.NewGuid(), FolderMediaType.Key, Constants.System.RootKey); + await MediaEditingService.CreateAsync(anotherRootCreateModel2, Constants.Security.SuperUserKey); + MediaNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); + + var sortingModels = initialRootKeys + .Reverse() + .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index }); + + var expectedSiblingsKeysList = initialRootKeys.Reverse().Where(k => k != node).ToList(); + + // Act + await MediaEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys); + var sortedSiblingsKeysList = sortedSiblingsKeys.ToList(); + + Assert.IsTrue(expectedSiblingsKeysList.SequenceEqual(sortedSiblingsKeysList)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 7f930c6144..ab9d579c9d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -188,6 +188,9 @@ DocumentNavigationServiceTests.cs + + DocumentNavigationServiceTests.cs + DocumentNavigationServiceTests.cs @@ -209,9 +212,15 @@ MediaNavigationServiceTests.cs + + DocumentNavigationServiceTests.cs + MediaNavigationServiceTests.cs + + MediaNavigationServiceTests.cs + MediaNavigationServiceTests.cs diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index 7d9a2e8397..86b52e2618 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -177,7 +177,7 @@ public class ContentNavigationServiceBaseTests [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 - public void Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] children) + public void Can_Get_Children_From_Existing_Content_Key_In_Their_Order_Of_Creation(Guid parentKey, string[] children) { // Arrange Guid[] expectedChildren = Array.ConvertAll(children, Guid.Parse); @@ -251,7 +251,7 @@ public class ContentNavigationServiceBaseTests [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 - public void Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] descendants) + public void Can_Get_Descendants_From_Existing_Content_Key_In_Their_Order_Of_Creation(Guid parentKey, string[] descendants) { // Arrange Guid[] expectedDescendants = Array.ConvertAll(descendants, Guid.Parse); @@ -318,7 +318,7 @@ public class ContentNavigationServiceBaseTests "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Great-grandchild 1 - public void Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] ancestors) + public void Can_Get_Ancestors_From_Existing_Content_Key_In_Their_Order_Of_Creation(Guid childKey, string[] ancestors) { // Arrange Guid[] expectedAncestors = Array.ConvertAll(ancestors, Guid.Parse); @@ -417,7 +417,7 @@ public class ContentNavigationServiceBaseTests [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF" })] // Child 1 - Child 2, Child 3 [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new[] { "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Grandchild 1 - Grandchild 2 - public void Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] siblings) + public void Can_Get_Siblings_Of_Existing_Content_Key_In_Their_Order_Of_Creation(Guid childKey, string[] siblings) { // Arrange Guid[] expectedSiblings = Array.ConvertAll(siblings, Guid.Parse); @@ -527,6 +527,30 @@ public class ContentNavigationServiceBaseTests }); } + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public void Moving_Node_To_Bin_Adds_It_To_Recycle_Bin_Root_As_The_Last_Item(Guid keyOfNodeToRemove) + { + // Arrange + Guid nodeInRecycleBin1 = Grandchild1; + Guid nodeInRecycleBin2 = Child3; + _navigationService.MoveToBin(nodeInRecycleBin1); + _navigationService.MoveToBin(nodeInRecycleBin2); + + // Act + _navigationService.MoveToBin(keyOfNodeToRemove); + + // Assert + _navigationService.TryGetSiblingsKeysInBin(nodeInRecycleBin1, out IEnumerable siblingsInBin); + + Assert.AreEqual(siblingsInBin.Last(), keyOfNodeToRemove); + } + [Test] [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 @@ -629,6 +653,30 @@ public class ContentNavigationServiceBaseTests }); } + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Adding_Node_To_Parent_Adds_It_As_The_Last_Child(Guid parentKey) + { + // Arrange + var newNodeKey = Guid.NewGuid(); + + // Act + _navigationService.Add(newNodeKey, parentKey); + + // Assert + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys); + + Assert.AreEqual(newNodeKey, childrenKeys.Last()); + } + [Test] public void Cannot_Move_Node_When_Target_Parent_Does_Not_Exist() { @@ -792,10 +840,32 @@ public class ContentNavigationServiceBaseTests } [Test] - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 1 to Child 2 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null, 1)] // Child 3 to content root - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 2 to Child 1 - public void Moved_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToMove, Guid? targetParentKey, int initialDescendantsCount) + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public void Moving_Node_To_Parent_Adds_It_As_The_Last_Child(Guid targetParentKey) + { + // Arrange + Guid nodeToMove = Grandchild4; + + // Act + _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable childrenKeys); + + Assert.AreEqual(nodeToMove, childrenKeys.Last()); + } + + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 1, "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1, null, 1)] // Child 3 to content root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 2, "C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 2 to Child 1 + public void Moved_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToMove, int sortOrder, Guid? targetParentKey, int initialDescendantsCount) { // Act var result = _navigationService.Move(nodeToMove, targetParentKey); @@ -811,10 +881,10 @@ public class ContentNavigationServiceBaseTests } [Test] - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Child 3 to Grandchild 2 - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 2 to Child 3 - [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Grandchild 1 to Child 2 - public void Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_Descendants(Guid nodeToMove, Guid targetParentKey, int initialDescendantsCountOfTargetParent) + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 0, "A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Child 3 to Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 1, "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 2 to Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 1, "60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Grandchild 1 to Child 2 + public void Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_Descendants(Guid nodeToMove, int sortOrder, Guid targetParentKey, int initialDescendantsCountOfTargetParent) { // Arrange // Get the number of descendants of the node to move