diff --git a/src/MenuTracker.cs b/src/MenuTracker.cs index ffe7d670..109c3b07 100644 --- a/src/MenuTracker.cs +++ b/src/MenuTracker.cs @@ -47,6 +47,22 @@ public void Register(MenuBar mb) this.bars.Add(mb); } + /// + /// Unregisters listeners for . + /// + /// to stop tracking. + public void UnregisterMenuBar( MenuBar? mb ) + { + if ( !bars.TryTake( out mb ) ) + { + return; + } + + mb.MenuAllClosed -= MenuAllClosed; + mb.MenuOpened -= MenuOpened; + mb.MenuClosing -= MenuClosing; + } + /// /// /// Searches child items of all MenuBars tracked by this class @@ -63,7 +79,7 @@ public void Register(MenuBar mb) /// The immediate parent of . /// Result may be a top level menu (e.g. File, View) /// or a sub-menu parent (e.g. View=>Windows). - public MenuBarItem? GetParent( MenuItem item, out MenuBar? hostBar ) + private MenuBarItem? GetParent( MenuItem item, out MenuBar? hostBar ) { foreach (var bar in this.bars) { @@ -83,7 +99,23 @@ public void Register(MenuBar mb) return null; } - public bool TryGetParent(MenuItem item, [NotNullWhen(true)]out MenuBar? hostBar, [NotNullWhen(true)] out MenuBarItem? parentItem) + /// + /// Searches child items of all MenuBars tracked by this class to try and find the parent of the item passed. + /// + /// The item whose parent you want to find. + /// + /// When this method returns true, the that owns .
Otherwise, if + /// not found or parent not registered (see ). + /// + /// + /// When this method returns , the immediate parent of .
Otherwise, + /// + /// + /// + /// Search is recursive and dips into sub-menus.
For sub-menus it is the immediate parent that is returned. + ///
+ /// A indicating if the search was successful or not. + public bool TryGetParent( MenuItem item, [NotNullWhen( true )] out MenuBar? hostBar, [NotNullWhen( true )] out MenuBarItem? parentItem ) { var parentCandidate = GetParent( item, out hostBar ); if ( parentCandidate is null ) @@ -106,22 +138,21 @@ public bool TryGetParent(MenuItem item, [NotNullWhen(true)]out MenuBar? hostBar, /// the substitution object (). See /// /// for more information. - public Dictionary ConvertEmptyMenus() + public Dictionary ConvertEmptyMenus( ) { - var toReturn = new Dictionary(); - + Dictionary dictionary = []; foreach (var b in this.bars) { foreach (var bi in b.Menus) { - foreach (var converted in this.ConvertEmptyMenus(b, bi)) + foreach ( ( MenuBarItem? convertedMenuBarItem, MenuItem? convertedMenuItem ) in this.ConvertEmptyMenus( dictionary, b, bi ) ) { - toReturn.Add(converted.Key, converted.Value); + dictionary.TryAdd( convertedMenuBarItem, convertedMenuItem ); } } } - return toReturn; + return dictionary; } /// @@ -137,12 +168,12 @@ public Dictionary ConvertEmptyMenus() /// The result of the conversion (same text, same index etc but /// instead of ). /// if conversion was possible (menu was empty and belonged to tracked menu). - internal static bool ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem bar, out MenuItem? added) + internal static bool ConvertMenuBarItemToRegularItemIfEmpty( MenuBarItem bar, [NotNullWhen( true )] out MenuItem? added ) { added = null; // bar still has more children so don't convert - if (bar.Children.Any()) + if ( bar.Children.Length != 0 ) { return false; } @@ -152,8 +183,7 @@ internal static bool ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem bar, out return false; } - var children = parent.Children.ToList(); - var idx = children.IndexOf(bar); + int idx = Array.IndexOf( parent.Children, bar ); if (idx < 0) { @@ -161,39 +191,32 @@ internal static bool ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem bar, out } // bar has no children so convert to MenuItem - added = new MenuItem { Title = bar.Title }; - added.Data = bar.Data; - added.Shortcut = bar.Shortcut; - - children.RemoveAt(idx); - children.Insert(idx, added); - - parent.Children = children.ToArray(); + parent.Children[ idx ] = added = new( ) + { + Title = bar.Title, + Data = bar.Data, + Shortcut = bar.Shortcut + }; return true; } /// - private Dictionary ConvertEmptyMenus(MenuBar bar, MenuBarItem mbi) + private Dictionary ConvertEmptyMenus(Dictionary dictionary, MenuBar bar, MenuBarItem mbi) { - var toReturn = new Dictionary(); - foreach (var c in mbi.Children.OfType()) { - this.ConvertEmptyMenus(bar, c); - if ( ConvertMenuBarItemToRegularItemIfEmpty( c, out var added)) + this.ConvertEmptyMenus(dictionary,bar, c); + if ( ConvertMenuBarItemToRegularItemIfEmpty( c, out MenuItem? added)) { - if (added != null) - { - toReturn.Add(c, added); - } + dictionary.TryAdd( c, added ); bar.CloseMenu(); bar.OpenMenu(); } } - return toReturn; + return dictionary; } private void MenuClosing(object? sender, MenuClosingEventArgs obj) @@ -204,7 +227,7 @@ private void MenuClosing(object? sender, MenuClosingEventArgs obj) private void MenuOpened(object? sender, MenuOpenedEventArgs obj) { this.CurrentlyOpenMenuItem = obj.MenuItem; - this.ConvertEmptyMenus(); + this.ConvertEmptyMenus( ); } private void MenuAllClosed(object? sender, EventArgs e) diff --git a/src/Operations/MenuOperations/RemoveMenuItemOperation.cs b/src/Operations/MenuOperations/RemoveMenuItemOperation.cs index 9bc1304c..35d696b2 100644 --- a/src/Operations/MenuOperations/RemoveMenuItemOperation.cs +++ b/src/Operations/MenuOperations/RemoveMenuItemOperation.cs @@ -64,10 +64,12 @@ public override void Undo() return; } - var children = this.Parent.Children.ToList(); - - children.Insert(this.removedAtIdx, this.OperateOn); - this.Parent.Children = children.ToArray(); + this.Parent.Children = + [ + .. Parent.Children[ .. removedAtIdx ], + this.OperateOn, + .. Parent.Children[ removedAtIdx .. ] + ]; this.Bar?.SetNeedsDisplay(); // if any MenuBarItem were converted to vanilla MenuItem @@ -123,12 +125,12 @@ protected override bool DoImpl() return false; } - var children = this.Parent.Children.ToList(); - - this.removedAtIdx = Math.Max(0, children.IndexOf(this.OperateOn)); - - children.Remove(this.OperateOn); - this.Parent.Children = children.ToArray(); + this.removedAtIdx = Math.Max( 0, Array.IndexOf( Parent.Children, OperateOn ) ); + this.Parent.Children = + [ + .. Parent.Children[ ..removedAtIdx ], + .. Parent.Children[ ( removedAtIdx + 1 ).. ] + ]; this.Bar?.SetNeedsDisplay(); if (this.Bar != null) @@ -137,27 +139,24 @@ protected override bool DoImpl() } // if a top level menu now has no children - if (this.Bar != null) + var empty = this.Bar?.Menus.Where(bi => bi.Children.Length == 0).ToArray(); + if (empty?.Any() == true) { - var empty = this.Bar.Menus.Where(bi => bi.Children.Length == 0).ToArray(); - if (empty.Any()) - { - // remember where they were - this.prunedEmptyTopLevelMenus = empty.ToDictionary(e => Array.IndexOf(this.Bar.Menus, e), v => v); + // remember where they were + this.prunedEmptyTopLevelMenus = empty.ToDictionary(e => Array.IndexOf(this.Bar.Menus, e), v => v); - // and remove them - this.Bar.Menus = this.Bar.Menus.Except(this.prunedEmptyTopLevelMenus.Values).ToArray(); - } + // and remove them + this.Bar.Menus = this.Bar.Menus.Except(this.prunedEmptyTopLevelMenus.Values).ToArray(); + } - // if we just removed the last menu header - // leaving a completely blank menu bar - if (this.Bar.Menus.Length == 0 && this.Bar.SuperView != null) - { - // remove the bar completely - this.Bar.CloseMenu(); - this.barRemovedFrom = this.Bar.SuperView; - this.barRemovedFrom.Remove(this.Bar); - } + // if we just removed the last menu header + // leaving a completely blank menu bar + if (this.Bar?.Menus.Length == 0 && this.Bar.SuperView != null) + { + // remove the bar completely + this.Bar.CloseMenu(); + this.barRemovedFrom = this.Bar.SuperView; + this.barRemovedFrom.Remove(this.Bar); } return true; diff --git a/src/ViewFactory.cs b/src/ViewFactory.cs index ac906cd7..096cbc49 100644 --- a/src/ViewFactory.cs +++ b/src/ViewFactory.cs @@ -28,7 +28,7 @@ public static class ViewFactory internal const string DefaultMenuItemText = "Edit Me"; internal static readonly Type[] KnownUnsupportedTypes = - { + [ typeof( Toplevel ), typeof( Dialog ), typeof( FileDialog ), @@ -42,7 +42,7 @@ public static class ViewFactory // BUG These seem to cause stack overflows in CreateSubControlDesigns (see TestAddView_RoundTrip) typeof( Wizard ), typeof( WizardStep ) - }; + ]; /// /// Gets a new instance of a default [], to include as the default initial @@ -57,12 +57,11 @@ internal static MenuBarItem[] DefaultMenuBarItems { get { - return new[] - { - new MenuBarItem( - "_File (F9)", - new[] { new MenuItem( DefaultMenuItemText, string.Empty, ( ) => { } ) } ) - }; + return + [ + new( "_File (F9)", + [ new MenuItem( DefaultMenuItemText, string.Empty, static ( ) => { } ) ] ) + ]; } } diff --git a/tests/MenuBarTests.cs b/tests/MenuBarTests.cs index a2a2e467..6eea6ee3 100644 --- a/tests/MenuBarTests.cs +++ b/tests/MenuBarTests.cs @@ -3,10 +3,394 @@ namespace UnitTests; [TestFixture] -[TestOf(typeof(OperationManager))] -[Category("UI")] +[TestOf( typeof( OperationManager ) )] +[Category( "UI" )] internal class MenuBarTests : Tests { + [Test] + [TestOf( typeof( RemoveMenuItemOperation ) )] + public void DeletingLastMenuItem_ShouldRemoveWholeBar( ) + { + using MenuBar bar = GetMenuBar( out Design root ); + + MenuItem mi = bar.Menus[ 0 ].Children[ 0 ]; + + RemoveMenuItemOperation? removeMenuItemOperation = null; + Assert.That( ( ) => removeMenuItemOperation = new( mi ), Throws.Nothing ); + Assert.That( removeMenuItemOperation, Is.Not.Null.And.InstanceOf( ) ); + + bool removeMenuItemOperationSucceeded = false; + Assert.That( ( ) => removeMenuItemOperationSucceeded = OperationManager.Instance.Do( removeMenuItemOperation! ), Throws.Nothing ); + Assert.That( removeMenuItemOperationSucceeded ); + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.UndoStackSize, Is.EqualTo( 1 ) ); + Assert.That( OperationManager.Instance.RedoStackSize, Is.Zero ); + } ); + + Assert.Multiple( ( ) => + { + Assert.That( bar.Menus, Is.Empty ); + Assert.That( root.View.Subviews, Has.None.InstanceOf( ) ); + } ); + + Assert.That( OperationManager.Instance.Undo, Throws.Nothing ); + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.UndoStackSize, Is.Zero ); + Assert.That( OperationManager.Instance.RedoStackSize, Is.EqualTo( 1 ) ); + // TODO: This needs to clean up after itself in a safe fashion + } ); + + // Same conditions as at the start + // The MenuBar should be back in the root view... + Assert.That( root.View.Subviews, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( root.View.Subviews[ 0 ], Is.Not.Null.And.SameAs( bar ) ); + + // ...And the original MenuBar should be back as it was at the start. + Assert.That( bar.Menus, Is.Not.Null ); + Assert.That( bar.Menus, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ], Is.Not.Null.And.InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ].Children, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.InstanceOf( ) ); + } + + [Test] + [TestOf( typeof( RemoveMenuItemOperation ) )] + public void DeletingMenuItemFromSubmenu_AllSubmenuChild( ) + { + using MenuBarWithSubmenuItems m = GetMenuBarWithSubmenuItems( ); + + MenuItem? bottomChild = m.Head2.Children[ 1 ]; + + Assume.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); + Assume.That( m.Bar.Menus[ 0 ].Children[ 1 ], Is.Not.Null.And.InstanceOf( ) ); + Assume.That( m.Head2.Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assume.That( m.Head2.Children[ 0 ], Is.SameAs( m.TopChild ) ); + + RemoveMenuItemOperation cmd1 = new( m.TopChild ); + Assert.That( cmd1.Do, Throws.Nothing ); + + RemoveMenuItemOperation cmd2 = new( bottomChild ); + Assert.That( cmd2.Do, Throws.Nothing ); + + // Deleting both children should convert us from + // a dropdown submenu to just a regular MenuItem + Assert.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); + Assert.That( m.Bar.Menus[ 0 ].Children[ 1 ], Is.Not.Null.And.InstanceOf( ) ); + + Assert.That( cmd2.Undo, Throws.Nothing ); + + // should bring the bottom one back + Assert.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); + Assert.That( m.Bar.Menus[ 0 ].Children[ 1 ], Is.Not.Null.And.InstanceOf( ) ); + Assert.That( ( (MenuBarItem)m.Bar.Menus[ 0 ].Children[ 1 ] ).Children[ 0 ], Is.SameAs( bottomChild ) ); + + Assert.That( cmd1.Undo, Throws.Nothing ); + + // Both submenu items should now be back + Assert.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( m.Bar.Menus[ 0 ].Children[ 1 ], Is.Not.Null.And.InstanceOf( ) ); + Assert.That( m.Head2.Children, Has.Exactly( 2 ).InstanceOf( ) ); + } ); + Assert.Multiple( ( ) => + { + Assert.That( m.Head2.Children[ 0 ], Is.SameAs( m.TopChild ) ); + Assert.That( ( (MenuBarItem)m.Bar.Menus[ 0 ].Children[ 1 ] ).Children[ 0 ], Is.SameAs( m.TopChild ) ); + Assert.That( ( (MenuBarItem)m.Bar.Menus[ 0 ].Children[ 1 ] ).Children[ 1 ], Is.SameAs( bottomChild ) ); + } ); + } + + [Test] + [TestOf( typeof( RemoveMenuItemOperation ) )] + public void DeletingMenuItemFromSubmenu_TopChild( ) + { + using MenuBarWithSubmenuItems m = GetMenuBarWithSubmenuItems( ); + + RemoveMenuItemOperation cmd = new ( m.TopChild ); + bool cmdSucceeded = false; + Assert.That( ( ) => cmdSucceeded = cmd.Do( ), Throws.Nothing ); + Assert.That( cmdSucceeded ); + + // Delete the top child should leave only 1 in submenu + Assert.Multiple( ( ) => + { + Assert.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); + Assert.That( m.Head2.Children, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( m.Head2.Children[ 0 ], Is.Not.SameAs( m.TopChild ) ); + } ); + + Assert.That( cmd.Undo, Throws.Nothing ); + + Assert.Multiple( ( ) => + { + // should come back now + Assert.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); + Assert.That( m.Head2.Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.That( m.Head2.Children[ 0 ], Is.SameAs( m.TopChild ) ); + } ); + } + + [Test] + [TestOf( typeof( MenuBarTests ) )] + [Category( "Change Control" )] + [Order( 1 )] + [Repeat( 10 )] + [Description( "Ensures that the GetMenuBar helper method returns the expected objects and doesn't fail even if used multiple times." )] + public void GetMenuBar_BehavesAsExpected( ) + { + using MenuBar bar = GetMenuBar( out Design root ); + Assert.Multiple( ( ) => + { + Assert.That( bar, Is.Not.Null.And.InstanceOf( ) ); + Assert.That( root, Is.Not.Null.And.InstanceOf( ) ); + } ); + Assert.That( root.View.Subviews, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( root.View.Subviews[ 0 ], Is.Not.Null.And.SameAs( bar ) ); + Assert.That( bar.Menus, Is.Not.Null ); + } ); + Assert.That( bar.Menus, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ], Is.Not.Null.And.InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ].Children, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( bar.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.InstanceOf( ) ); + Assert.That( OperationManager.Instance.UndoStackSize, Is.Zero ); + Assert.That( OperationManager.Instance.RedoStackSize, Is.Zero ); + } ); + } + + [Test] + [TestOf( typeof( MenuBarWithSubmenuItems ) )] + [Category( "Change Control" )] + [Order( 2 )] + [Repeat( 10 )] + [Description( "Ensures that the GetMenuBarWithSubmenuItems helper method returns the expected objects and doesn't fail even if used multiple times." )] + public void GetMenuBarWithSubmenuItems_BehavesAsExpected( ) + { + using MenuBarWithSubmenuItems m = GetMenuBarWithSubmenuItems( ); + + Assert.That( m.Bar.Menus, Has.Exactly( 1 ).InstanceOf( ) ); + + MenuBarItem menu0 = m.Bar.Menus[ 0 ]; + Assert.That( menu0.Children, Has.Exactly( 3 ).InstanceOf( ) ); + + // First item + MenuItem menu0Child0 = menu0.Children[ 0 ]; + Assert.That( menu0Child0.Title, Is.EqualTo( "Head1" ) ); + + // Second item and its children + Assert.That( menu0.Children[ 1 ], Is.Not.Null.And.InstanceOf( ) ); + MenuBarItem menu0Child1 = (MenuBarItem)menu0.Children[ 1 ]; + Assert.Multiple( ( ) => + { + Assert.That( menu0Child1.Title, Is.EqualTo( "Head2" ) ); + Assert.That( menu0Child1.Children, Has.Exactly( 2 ).InstanceOf( ) ); + } ); + MenuItem menu0Child1Leaf0 = menu0Child1.Children[ 0 ]; + MenuItem menu0Child1Leaf1 = menu0Child1.Children[ 1 ]; + Assert.Multiple( ( ) => + { + Assert.That( menu0Child1Leaf0.Title, Is.EqualTo( "Child1" ) ); + Assert.That( menu0Child1Leaf0.Shortcut, Is.EqualTo( Key.J.WithCtrl.KeyCode ) ); + Assert.That( menu0Child1Leaf1.Title, Is.EqualTo( "Child2" ) ); + Assert.That( menu0Child1Leaf1.Shortcut, Is.EqualTo( Key.F.WithCtrl.KeyCode ) ); + } ); + + // Third item + Assert.That( menu0.Children[ 2 ], Is.Not.Null.And.InstanceOf( ) ); + MenuItem menu0Child2 = menu0.Children[ 2 ]; + Assert.That( menu0Child2.Title, Is.EqualTo( "Head3" ) ); + + //Now just make sure the record properties were set to the right references + Assert.Multiple( ( ) => + { + Assert.That( m.Head2, Is.SameAs( menu0Child1 ) ); + Assert.That( m.TopChild, Is.SameAs( menu0Child1Leaf0 ) ); + } ); + } + + [Test] + [TestOf( typeof( MoveMenuItemLeftOperation ) )] + public void MoveMenuItemLeft_CannotMoveRootItems( ) + { + using MenuBar bar = GetMenuBar( ); + + // cannot move a root item + MoveMenuItemLeftOperation moveMenuItemLeftOperation = new( bar.Menus[ 0 ].Children[ 0 ] ); + Assert.That( moveMenuItemLeftOperation.IsImpossible ); + bool moveMenuItemLeftOperationSucceeded = false; + Assert.That( ( ) => moveMenuItemLeftOperationSucceeded = moveMenuItemLeftOperation.Do( ), Throws.Nothing ); + Assert.That( moveMenuItemLeftOperationSucceeded, Is.False ); + } + + [Test] + public void MoveMenuItemLeft_MoveTopChild( ) + { + using MenuBarWithSubmenuItems m = GetMenuBarWithSubmenuItems( ); + + MoveMenuItemLeftOperation moveMenuItemLeftOperation = new ( m.TopChild ); + Assert.That( moveMenuItemLeftOperation.IsImpossible, Is.False ); + bool moveMenuItemLeftOperationSucceeded = false; + Assert.That( ( ) => moveMenuItemLeftOperationSucceeded = moveMenuItemLeftOperation.Do( ), Throws.Nothing ); + Assert.That( moveMenuItemLeftOperationSucceeded ); + + // move the top child left should pull + // it out of the submenu and onto the root + Assert.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 4 ).InstanceOf( ) ); + Assert.That( m.Head2.Children, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( m.Head2.Children[ 0 ], Is.Not.SameAs( m.TopChild ) ); + + // it should be pulled out underneath its parent + Assert.That( m.Bar.Menus[ 0 ].Children[ 2 ], Is.SameAs( m.TopChild ) ); + + // undoing command should return us to previous state + Assert.That( moveMenuItemLeftOperation.Undo, Throws.Nothing ); + + Assert.That( m.Bar.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); + Assert.That( m.Head2.Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.That( m.Head2.Children[ 0 ], Is.SameAs( m.TopChild ) ); + } + + [Test] + [TestOf( typeof( AddMenuItemOperation ) )] + public void MoveMenuItemRight_CannotMoveElementZero( ) + { + using MenuBar bar = GetMenuBar( ); + + MenuItem? mi = bar.Menus[ 0 ].Children[ 0 ]; + mi.Data = "yarg"; + mi.Shortcut = Key.Y.WithCtrl.KeyCode; + + AddMenuItemOperation addAnother = new( mi ); + Assert.That( addAnother.IsImpossible, Is.False ); + bool addAnotherSucceeded = false; + Assert.That( ( ) => addAnotherSucceeded = addAnother.Do( ), Throws.Nothing ); + Assert.That( addAnotherSucceeded ); + + // should have added below us + Assert.That( bar.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( bar.Menus[ 0 ].Children[ 0 ], Is.SameAs( mi ) ); + Assert.That( bar.Menus[ 0 ].Children[ 1 ], Is.Not.SameAs( mi ) ); + } ); + + // cannot move element 0 + MoveMenuItemRightOperation impossibleMoveRightOp = new( bar.Menus[ 0 ].Children[ 0 ] ); + Assert.That( impossibleMoveRightOp.IsImpossible ); + bool impossibleMoveRightOpSucceeded = false; + Assert.That( ( ) => impossibleMoveRightOpSucceeded = impossibleMoveRightOp.Do( ), Throws.Nothing ); + Assert.That( impossibleMoveRightOpSucceeded, Is.False ); + + // can move element 1 + // This is a destructive action, so references will change. + MoveMenuItemRightOperation validMoveRightOp = new( bar.Menus[ 0 ].Children[ 1 ] ); + Assert.That( validMoveRightOp.IsImpossible, Is.False ); + bool validMoveRightOpSucceeded = false; + Assert.That( ( ) => validMoveRightOpSucceeded = validMoveRightOp.Do( ), Throws.Nothing ); + Assert.That( validMoveRightOpSucceeded ); + + // We will have changed from a MenuItem to a MenuBarItem + // so element 0 will not be us. In Terminal.Gui there is + // a different class for a menu item and one with submenus. + Assert.That( bar.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.InstanceOf( ) ); + MenuBarItem miConvertedToMenuBarItem = (MenuBarItem)bar.Menus[ 0 ].Children[ 0 ]; + + // Check that the references are unequal but values are equal + Assert.Multiple( ( ) => + { + Assert.That( miConvertedToMenuBarItem, Is.Not.SameAs( mi ) ); + Assert.That( miConvertedToMenuBarItem.Title, Is.EqualTo( mi.Title ) ); + Assert.That( miConvertedToMenuBarItem.Data, Is.EqualTo( mi.Data ) ); + Assert.That( miConvertedToMenuBarItem.Children, Has.Exactly( 1 ).InstanceOf( ) ); + } ); + + // Now undo it. + // This is destructive as well. + Assert.That( validMoveRightOp.Undo, Throws.Nothing ); + + Assert.That( bar.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( bar.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ].Children[ 1 ], Is.Not.Null.And.InstanceOf( ) ); + } ); + MenuItem firstChildAfterUndo = bar.Menus[ 0 ].Children[ 0 ]; + MenuItem secondChildAfterUndo = bar.Menus[ 0 ].Children[ 1 ]; + + Assert.Multiple( ( ) => + { + // All the previous references are gone forever through this process. + // So, neither element should be mi. + Assert.That( firstChildAfterUndo, Is.Not.SameAs( mi ) ); + Assert.That( secondChildAfterUndo, Is.Not.SameAs( mi ) ); + + // Neither element should be miConvertedToMenuBarItem either + Assert.That( firstChildAfterUndo, Is.Not.SameAs( miConvertedToMenuBarItem ) ); + Assert.That( secondChildAfterUndo, Is.Not.SameAs( miConvertedToMenuBarItem ) ); + + // And mi still should not be miConvertedToMenuBarItem + Assert.That( mi, Is.Not.SameAs( miConvertedToMenuBarItem ) ); + + // But the values need to be preserved + Assert.That( firstChildAfterUndo.Title, Is.EqualTo( mi.Title ) ); + Assert.That( firstChildAfterUndo.Data, Is.EqualTo( mi.Data ) ); + Assert.That( firstChildAfterUndo.Shortcut, Is.EqualTo( mi.Shortcut ) ); + } ); + } + + /// + /// Tests that when there is only one menu item + /// that it cannot be moved into a submenu + /// + [Test] + [TestOf( typeof( MoveMenuItemRightOperation ) )] + public void MoveMenuItemRight_CannotMoveLast( ) + { + MenuBar bar = GetMenuBar( ); + + MenuItem? mi = bar.Menus[ 0 ].Children[ 0 ]; + MoveMenuItemRightOperation cmd = new( mi ); + Assert.That( cmd.IsImpossible ); + Assert.That( cmd.Do, Is.False ); + } + + /// + /// Tests removing the last menu item (i.e. 'Do Something') + /// under the only remaining menu header (e.g. 'File F9') + /// should result in a completely empty menu bar and be undoable + /// + [Test] + [TestOf( typeof( RemoveMenuItemOperation ) )] + public void RemoveFinalMenuItemOnBar( ) + { + using MenuBar bar = GetMenuBar( ); + + MenuBarItem? fileMenu = bar.Menus[ 0 ]; + MenuItem? placeholderMenuItem = fileMenu.Children[ 0 ]; + + RemoveMenuItemOperation removeOp = new( placeholderMenuItem ); + + // we are able to remove the last one + Assert.That( removeOp.IsImpossible, Is.False ); + bool removeOpSucceeded = false; + Assert.That( ( ) => removeOpSucceeded = removeOp.Do( ), Throws.Nothing ); + Assert.That( removeOpSucceeded ); + Assert.That( bar.Menus, Is.Empty ); + + Assert.That( removeOp.Undo, Throws.Nothing ); + + // should be back to where we started + Assert.That( bar.Menus, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ].Children, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( bar.Menus[ 0 ].Children[ 0 ], Is.SameAs( placeholderMenuItem ) ); + } + [Test] public void RoundTrip_PreserveMenuItems() { @@ -31,7 +415,7 @@ public void RoundTrip_PreserveMenuItems() Assume.That( addViewOperation, Is.Not.Null.And.InstanceOf( ) ); bool addViewOperationSucceeded = false; - Assert.That( ( ) => addViewOperationSucceeded = OperationManager.Instance.Do( addViewOperation ), Throws.Nothing ); + Assert.That( ( ) => addViewOperationSucceeded = OperationManager.Instance.Do( addViewOperation! ), Throws.Nothing ); Assert.That( addViewOperationSucceeded ); Assume.That( ( ) => viewToCode.GenerateDesignerCs( designOut, typeof( Dialog ) ), Throws.Nothing ); @@ -41,19 +425,19 @@ public void RoundTrip_PreserveMenuItems() Assert.That( codeToView, Is.Not.Null.And.InstanceOf( ) ); Design? designBackIn = null; - Assert.That( ( ) => designBackIn = codeToView.CreateInstance( ), Throws.Nothing ); + Assert.That( ( ) => designBackIn = codeToView!.CreateInstance( ), Throws.Nothing ); Assert.That( designBackIn, Is.Not.Null.And.InstanceOf( ) ); // 1 visible root menu (e.g. File) MenuBar? mbIn = null; - Assert.That( designBackIn.View, Is.Not.Null.And.InstanceOf( ) ); + Assert.That( designBackIn!.View, Is.Not.Null.And.InstanceOf( ) ); IList actualSubviews = designBackIn.View.GetActualSubviews(); Assert.That( actualSubviews, Has.Exactly( 1 ).InstanceOf( ) ); Assert.That( ( ) => mbIn = actualSubviews.OfType( ).Single( ), Throws.Nothing ); Assert.That( mbIn, Is.Not.Null.And.InstanceOf( ) ); // 1 child menu item (e.g. Open) - Assert.That( mbIn.Menus, Is.Not.Null.And.Not.Empty ); + Assert.That( mbIn!.Menus, Is.Not.Null.And.Not.Empty ); Assert.That( mbIn.Menus, Has.Exactly( 1 ).InstanceOf( ) ); Assert.That( mbIn.Menus[ 0 ].Children, Has.Exactly( 1 ).InstanceOf( ) ); Assert.That( mbIn.Menus[ 0 ].Children[ 0 ].Title, Is.EqualTo( mbOut.Menus[ 0 ].Children[ 0 ].Title ) ); @@ -91,11 +475,11 @@ public void RoundTrip_PreserveMenuItems_WithSubmenus( ) bool addChildMenuOperation1Succeeded = false; bool addChildMenuOperation2Succeeded = false; bool addChildMenuOperation3Succeeded = false; - Assert.That( ( ) => addChildMenuOperation1Succeeded = addChildMenuOperation1.Do( ), Throws.Nothing ); + Assert.That( ( ) => addChildMenuOperation1Succeeded = addChildMenuOperation1!.Do( ), Throws.Nothing ); Assert.That( addChildMenuOperation1Succeeded ); - Assert.That( ( ) => addChildMenuOperation2Succeeded = addChildMenuOperation2.Do( ), Throws.Nothing ); + Assert.That( ( ) => addChildMenuOperation2Succeeded = addChildMenuOperation2!.Do( ), Throws.Nothing ); Assert.That( addChildMenuOperation2Succeeded ); - Assert.That( ( ) => addChildMenuOperation3Succeeded = addChildMenuOperation3.Do( ), Throws.Nothing ); + Assert.That( ( ) => addChildMenuOperation3Succeeded = addChildMenuOperation3!.Do( ), Throws.Nothing ); Assert.That( addChildMenuOperation3Succeeded ); } ); @@ -122,7 +506,7 @@ public void RoundTrip_PreserveMenuItems_WithSubmenus( ) Design? designBackIn = null; - Assume.That( ( ) => designBackIn = codeToView.CreateInstance( ), Throws.Nothing ); + Assume.That( ( ) => designBackIn = codeToView!.CreateInstance( ), Throws.Nothing ); Assume.That( designBackIn, Is.Not.Null.And.InstanceOf( ) ); MenuBar? mbIn = null; @@ -134,7 +518,7 @@ public void RoundTrip_PreserveMenuItems_WithSubmenus( ) Assert.That( mbIn, Is.Not.Null.And.InstanceOf( ) ); // 1 visible root menu (e.g. File) - Assert.That( mbIn.Menus, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( mbIn!.Menus, Has.Exactly( 1 ).InstanceOf( ) ); // 3 child menu item (original one + 3 we added -1 because we moved it to submenu) Assert.That( mbIn.Menus[ 0 ].Children, Has.Exactly( 3 ).InstanceOf( ) ); Assert.That( mbIn.Menus[ 0 ].Children, Has.All.Not.Null ); @@ -147,18 +531,21 @@ public void RoundTrip_PreserveMenuItems_WithSubmenus( ) } [Test] - [TestOf(typeof(MenuTracker))] - public void TestMenuOperations() + [TestOf( typeof( MenuTracker ) )] + // TODO: Break this one up into smaller units at some point. + public void TestMenuOperations( ) { - ViewToCode viewToCode = new (); + ViewToCode viewToCode = new( ); - FileInfo file = new ($"{nameof(TestMenuOperations)}.cs"); + FileInfo file = new( $"{nameof( TestMenuOperations )}.cs" ); Design designOut = viewToCode.GenerateNewView( file, "YourNamespace", typeof( Dialog ) ); Assume.That( designOut, Is.Not.Null.And.InstanceOf( ) ); Assume.That( designOut.View, Is.Not.Null.And.InstanceOf( ) ); - MenuBar mbOut = ViewFactory.Create( ); + using MenuBar mbOut = ViewFactory.Create( ); Assume.That( mbOut, Is.Not.Null.And.InstanceOf( ) ); + Assume.That( OperationManager.Instance.UndoStackSize, Is.Zero ); + Assume.That( OperationManager.Instance.RedoStackSize, Is.Zero ); Assert.Warn( "MenuTracker.Instance.CurrentlyOpenMenuItem cannot be guaranteed null at this time. See https://github.com/gui-cs/TerminalGuiDesigner/issues/270" ); // TODO: Enable this pre-condition once MenuTracker changes are implemented. @@ -168,192 +555,196 @@ public void TestMenuOperations() MenuTracker.Instance.Register( mbOut ); // 1 visible root menu (e.g. File) - ClassicAssert.AreEqual(1, mbOut.Menus.Length); + Assert.That( mbOut.Menus, Has.Exactly( 1 ).InstanceOf( ) ); // 1 child menu item (e.g. Open) - ClassicAssert.AreEqual(1, mbOut.Menus[0].Children.Length); + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 1 ).InstanceOf( ) ); - MenuItem? orig = mbOut.Menus[0].Children[0]; + MenuItem? orig = mbOut.Menus[ 0 ].Children[ 0 ]; + Assert.That( orig, Is.Not.Null.And.InstanceOf( ) ); - OperationManager.Instance.Do( - new AddMenuItemOperation(mbOut.Menus[0].Children[0])); + AddMenuItemOperation? addMenuItemOperation = null; + Assert.That( ( ) => addMenuItemOperation = new( mbOut.Menus[ 0 ].Children[ 0 ] ), Throws.Nothing ); + Assert.That( addMenuItemOperation, Is.Not.Null.And.InstanceOf( ) ); + + bool addMenuItemOperationSucceeded = false; + Assert.That( ( ) => addMenuItemOperationSucceeded = OperationManager.Instance.Do( addMenuItemOperation! ), Throws.Nothing ); + Assert.That( addMenuItemOperationSucceeded ); // Now 2 child menu item - ClassicAssert.AreEqual(2, mbOut.Menus[0].Children.Length); - ClassicAssert.AreSame(orig, mbOut.Menus[0].Children[0]); // original is still at top + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.SameAs( orig ) ); // original is still at top + Assert.That( mbOut.Menus[ 0 ].Children[ 1 ], Is.Not.Null.And.Not.SameAs( orig ) ); + } ); + + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.UndoStackSize, Is.EqualTo( 1 ) ); + Assert.That( OperationManager.Instance.RedoStackSize, Is.Zero ); + } ); OperationManager.Instance.Undo(); + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.UndoStackSize, Is.Zero ); + Assert.That( OperationManager.Instance.RedoStackSize, Is.EqualTo( 1 ) ); + } ); + // Now only 1 child menu item - ClassicAssert.AreEqual(1, mbOut.Menus[0].Children.Length); - ClassicAssert.AreSame(orig, mbOut.Menus[0].Children[0]); // original is still at top + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 1 ).InstanceOf( ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.SameAs( orig ) ); // original is still at top OperationManager.Instance.Redo(); - // Now 2 child menu item - ClassicAssert.AreEqual(2, mbOut.Menus[0].Children.Length); - ClassicAssert.AreSame(orig, mbOut.Menus[0].Children[0]); // original is still at top + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.UndoStackSize, Is.EqualTo( 1 ) ); + Assert.That( OperationManager.Instance.RedoStackSize, Is.Zero ); + } ); + + // Now 2 child menu items again + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.SameAs( orig ) ); // original is still at top + Assert.That( mbOut.Menus[ 0 ].Children[ 1 ], Is.Not.Null.And.Not.SameAs( orig ) ); // original is still at top + } ); // Now test moving an item around - MenuItem? toMove = mbOut.Menus[0].Children[1]; + MenuItem? toMove = mbOut.Menus[ 0 ].Children[ 1 ]; + Assume.That( toMove, Is.Not.Null.And.InstanceOf( ) ); // Move second menu item up - MoveMenuItemOperation up = new MoveMenuItemOperation(toMove, true); - ClassicAssert.IsFalse(up.IsImpossible); - OperationManager.Instance.Do(up); + MoveMenuItemOperation? up = null; + Assert.That( ( ) => up = new( toMove, true ), Throws.Nothing ); + Assert.That( up, Is.Not.Null.And.InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( up!.Bar, Is.SameAs( mbOut ) ); + Assert.That( up.IsImpossible, Is.False ); + } ); + + bool moveUpSucceeded = false; + Assert.That( ( ) => moveUpSucceeded = OperationManager.Instance.Do( up! ), Throws.Nothing ); + Assert.That( moveUpSucceeded ); + + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.UndoStackSize, Is.EqualTo( 2 ) ); + Assert.That( OperationManager.Instance.RedoStackSize, Is.Zero ); + } ); // Original one should now be bottom - ClassicAssert.AreSame(orig, mbOut.Menus[0].Children[1]); + Assume.That( orig, Is.Not.SameAs( toMove ) ); + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.Not.SameAs( orig ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.Null.And.SameAs( toMove ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 1 ], Is.SameAs( orig ) ); + } ); // can't move top one up - ClassicAssert.IsTrue(new MoveMenuItemOperation(toMove, true).IsImpossible); + MoveMenuItemOperation? impossibleMoveUpOperation = null; + Assert.That( ( ) => impossibleMoveUpOperation = new( toMove, true ), Throws.Nothing ); + Assert.That( impossibleMoveUpOperation, Is.Not.Null.And.InstanceOf( ) ); + Assert.That( impossibleMoveUpOperation!.IsImpossible ); + // cant move bottom one down - ClassicAssert.IsTrue(new MoveMenuItemOperation(mbOut.Menus[0].Children[1], false).IsImpossible); + MoveMenuItemOperation? impossibleMoveDownOperation = null; + Assert.That( ( ) => impossibleMoveDownOperation = new( mbOut.Menus[ 0 ].Children[ 1 ], false ), Throws.Nothing ); + Assert.That( impossibleMoveDownOperation, Is.Not.Null.And.InstanceOf( ) ); + Assert.That( impossibleMoveDownOperation!.IsImpossible ); - OperationManager.Instance.Undo(); + Assert.That( static ( ) => OperationManager.Instance.Undo( ), Throws.Nothing ); // Original one should be back on top - ClassicAssert.AreSame(orig, mbOut.Menus[0].Children[0]); + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.SameAs( orig ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.SameAs( toMove ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 1 ], Is.SameAs( toMove ) ); + } ); // test moving the top one down - MenuItem? toMove2 = mbOut.Menus[0].Children[1]; + MenuItem? toMove2 = mbOut.Menus[ 0 ].Children[ 1 ]; // Move first menu item down - MoveMenuItemOperation down = new MoveMenuItemOperation(toMove2, true); - ClassicAssert.IsFalse(down.IsImpossible); - OperationManager.Instance.Do(down); - - // Original one should now be bottom - ClassicAssert.AreSame(orig, mbOut.Menus[0].Children[1]); - ClassicAssert.AreNotSame(orig, mbOut.Menus[0].Children[0]); - - OperationManager.Instance.Undo(); - - // should be back to how we started now - ClassicAssert.AreSame(orig, mbOut.Menus[0].Children[0]); - ClassicAssert.AreNotSame(orig, mbOut.Menus[0].Children[1]); - } - - private MenuBar GetMenuBar() - { - return this.GetMenuBar(out _); - } - - private MenuBar GetMenuBar(out Design root) - { - root = Get10By10View(); - - var bar = ViewFactory.Create( ); - var addBarCmd = new AddViewOperation(bar, root, "mb"); - ClassicAssert.IsTrue(addBarCmd.Do()); + MoveMenuItemOperation? down = null; + Assert.That( ( ) => down = new( toMove2, true ), Throws.Nothing ); + Assert.That( down, Is.Not.Null.And.InstanceOf( ) ); + Assert.That( down!.IsImpossible, Is.False ); - // Expect ViewFactory to have created a single - // placeholder menu item - ClassicAssert.AreEqual(1, bar.Menus.Length); - ClassicAssert.AreEqual(1, bar.Menus[0].Children.Length); - - return bar; - } - - /// - /// Tests removing the last menu item (i.e. 'Do Something') - /// under the only remaining menu header (e.g. 'File F9') - /// should result in a completely empty menu bar and be undoable - /// - [Test] - public void TestRemoveFinalMenuItemOnBar() - { - var bar = this.GetMenuBar(); + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.RedoStackSize, Is.EqualTo( 1 ) ); + Assert.That( OperationManager.Instance.UndoStackSize, Is.EqualTo( 1 ) ); + } ); - var fileMenu = bar.Menus[0]; - var placeholderMenuItem = fileMenu.Children[0]; + bool moveDownSucceeded = false; + Assert.That( ( ) => moveDownSucceeded = OperationManager.Instance.Do( down ), Throws.Nothing ); + Assert.That( moveDownSucceeded ); + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.RedoStackSize, Is.Zero ); + Assert.That( OperationManager.Instance.UndoStackSize, Is.EqualTo( 2 ) ); + } ); - var remove = new RemoveMenuItemOperation(placeholderMenuItem); + // Original one should now be bottom + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.SameAs( toMove2 ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.SameAs( orig ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 1 ], Is.SameAs( orig ) ); + } ); - // we are able to remove the last one - ClassicAssert.IsTrue(remove.Do()); - ClassicAssert.IsEmpty(bar.Menus, "menu bar should now be completely empty"); + Assert.That( static ( ) => OperationManager.Instance.Undo( ), Throws.Nothing ); - remove.Undo(); + Assert.Multiple( static ( ) => + { + Assert.That( OperationManager.Instance.RedoStackSize, Is.EqualTo( 1 ) ); + Assert.That( OperationManager.Instance.UndoStackSize, Is.EqualTo( 1 ) ); + } ); - // should be back to where we started - ClassicAssert.AreEqual(1, bar.Menus.Length); - ClassicAssert.AreEqual(1, bar.Menus[0].Children.Length); - ClassicAssert.AreSame(placeholderMenuItem, bar.Menus[0].Children[0]); - } - /// - /// Tests that when there is only one menu item - /// that it cannot be moved into a submenu - /// - [Test] - public void TestMoveMenuItemRight_CannotMoveLast() - { - var bar = this.GetMenuBar(); + // should be back to how we started now + Assert.That( mbOut.Menus[ 0 ].Children, Has.Exactly( 2 ).InstanceOf( ) ); + Assert.Multiple( ( ) => + { + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.SameAs( orig ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 0 ], Is.Not.SameAs( toMove2 ) ); + Assert.That( mbOut.Menus[ 0 ].Children[ 1 ], Is.SameAs( toMove2 ) ); + } ); - var mi = bar.Menus[0].Children[0]; - var cmd = new MoveMenuItemRightOperation(mi); - ClassicAssert.IsFalse(cmd.Do()); + MenuTracker.Instance.UnregisterMenuBar( mbOut ); } - [Test] - public void TestMoveMenuItemRight_CannotMoveElementZero() + private static MenuBar GetMenuBar( ) { - var bar = this.GetMenuBar(); - - var mi = bar.Menus[0].Children[0]; - mi.Data = "yarg"; - mi.Shortcut = Key.Y.WithCtrl.KeyCode; - var addAnother = new AddMenuItemOperation(mi); - ClassicAssert.True(addAnother.Do()); - - // should have added below us - ClassicAssert.AreSame(mi, bar.Menus[0].Children[0]); - ClassicAssert.AreNotSame(mi, bar.Menus[0].Children[1]); - ClassicAssert.AreEqual(2, bar.Menus[0].Children.Length); - - // cannot move element 0 - ClassicAssert.IsFalse(new MoveMenuItemRightOperation( - bar.Menus[0].Children[0]) - .Do()); - - var cmd = new MoveMenuItemRightOperation( - bar.Menus[0].Children[1]); - - // can move element 1 - ClassicAssert.IsTrue(cmd.Do()); - - // We will have changed from a MenuItem to a MenuBarItem - // so element 0 will not be us. In Terminal.Gui there is - // a different class for a menu item and one with submenus - ClassicAssert.AreNotSame(mi, bar.Menus[0].Children[0]); - ClassicAssert.AreEqual(mi.Title, bar.Menus[0].Children[0].Title); - ClassicAssert.AreEqual(mi.Data, bar.Menus[0].Children[0].Data); - ClassicAssert.AreEqual(1, bar.Menus[0].Children.Length); - - cmd.Undo(); - - ClassicAssert.AreEqual(mi.Title, bar.Menus[0].Children[0].Title); - ClassicAssert.AreEqual(mi.Data, bar.Menus[0].Children[0].Data); - ClassicAssert.AreEqual(mi.Shortcut, bar.Menus[0].Children[0].Shortcut); - ClassicAssert.AreNotSame(mi, bar.Menus[0].Children[1]); + return GetMenuBar( out _ ); } - [Test] - public void TestMoveMenuItemLeft_CannotMoveRootItems() + private static MenuBar GetMenuBar( out Design root ) { - var bar = this.GetMenuBar(); + root = Get10By10View( ); - var mi = bar.Menus[0].Children[0]; + var bar = ViewFactory.Create( ); + var addBarCmd = new AddViewOperation( bar, root, "mb" ); + addBarCmd.Do( ); - // cannot move a root item - ClassicAssert.IsFalse(new MoveMenuItemLeftOperation( - bar.Menus[0].Children[0]) - .Do()); + return bar; } - private MenuBar GetMenuBarWithSubmenuItems(out MenuBarItem head2, out MenuItem topChild) + private static MenuBarWithSubmenuItems GetMenuBarWithSubmenuItems( ) { - var bar = this.GetMenuBar(); + MenuBarWithSubmenuItems toReturn = new( GetMenuBar( ), null!, null! ) + { + Bar = GetMenuBar( ) + }; // Set up a menu like: /* @@ -363,156 +754,54 @@ private MenuBar GetMenuBarWithSubmenuItems(out MenuBarItem head2, out MenuItem t Head3 Child2 */ - var mi = bar.Menus[0].Children[0]; + var mi = toReturn.Bar.Menus[ 0 ].Children[ 0 ]; mi.Title = "Head1"; - bar.Menus[0].Children = new[] + toReturn.Bar.Menus[ 0 ].Children = + [ + toReturn.Bar.Menus[ 0 ].Children[ 0 ], + toReturn.Head2 = CreateHead2Item( ), + new( "Head3", null, static ( ) => { } ), + ]; + + return toReturn; + + MenuBarItem CreateHead2Item( ) { - bar.Menus[0].Children[0], - head2 = new MenuBarItem(new[] + return new( [toReturn.TopChild = CreateHead2Child1Item( ), CreateHead2Child2Item( )] ) { - topChild = new MenuItem("Child1", null, () => { }) + Title = "Head2", + }; + + static MenuItem CreateHead2Child1Item( ) + { + return new( "Child1", null, static ( ) => { } ) { Data = "Child1", Shortcut = Key.J.WithCtrl.KeyCode, - }, - new MenuItem("Child2", null, () => { }) + }; + } + + static MenuItem CreateHead2Child2Item( ) + { + return new( "Child2", null, static ( ) => { } ) { Data = "Child2", Shortcut = Key.F.WithCtrl.KeyCode, - }, - }) - { - Title = "Head2", - }, - new MenuItem("Head3", null, () => { }), - }; - - return bar; + }; + } + } } - [Test] - public void TestMoveMenuItemLeft_MoveTopChild() + private sealed record MenuBarWithSubmenuItems( MenuBar Bar, MenuBarItem Head2, MenuItem TopChild ) : IDisposable { - var bar = this.GetMenuBarWithSubmenuItems(out var head2, out var topChild); - - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(2, head2.Children.Length); - ClassicAssert.AreSame(topChild, head2.Children[0]); - - var cmd = new MoveMenuItemLeftOperation(topChild); - ClassicAssert.IsTrue(cmd.Do()); + public MenuBarItem Head2 { get; set; } = Head2; + public MenuItem TopChild { get; set; } = TopChild; - // move the top child left should pull - // it out of the submenu and onto the root - ClassicAssert.AreEqual(4, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(1, head2.Children.Length); - - // it should be pulled out underneath its parent - // and preserve its (Name) and Shortcuts - ClassicAssert.AreEqual(topChild.Title, bar.Menus[0].Children[2].Title); - ClassicAssert.AreEqual(topChild.Data, bar.Menus[0].Children[2].Data); - ClassicAssert.AreEqual(topChild.Shortcut, bar.Menus[0].Children[2].Shortcut); - ClassicAssert.AreSame(topChild, bar.Menus[0].Children[2]); - - // undoing command should return us to - // previous state - cmd.Undo(); - - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(2, head2.Children.Length); - - ClassicAssert.AreEqual(topChild.Title, head2.Children[0].Title); - ClassicAssert.AreEqual(topChild.Data, head2.Children[0].Data); - ClassicAssert.AreEqual(topChild.Shortcut, head2.Children[0].Shortcut); - ClassicAssert.AreSame(topChild, head2.Children[0]); - } - - [Test] - public void TestDeletingMenuItemFromSubmenu_TopChild() - { - var bar = this.GetMenuBarWithSubmenuItems(out var head2, out var topChild); - - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(2, head2.Children.Length); - ClassicAssert.AreSame(topChild, head2.Children[0]); - - var cmd = new RemoveMenuItemOperation(topChild); - ClassicAssert.IsTrue(cmd.Do()); - - // Delete the top child should leave only 1 in submenu - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(1, head2.Children.Length); - ClassicAssert.AreNotSame(topChild, head2.Children[0]); - - cmd.Undo(); - - // should come back now - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(2, head2.Children.Length); - ClassicAssert.AreSame(topChild, head2.Children[0]); - } - - [Test] - public void TestDeletingMenuItemFromSubmenu_AllSubmenuChild() - { - var bar = this.GetMenuBarWithSubmenuItems(out var head2, out var topChild); - var bottomChild = head2.Children[1]; - - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(typeof(MenuBarItem), bar.Menus[0].Children[1].GetType()); - ClassicAssert.AreEqual(2, head2.Children.Length); - ClassicAssert.AreSame(topChild, head2.Children[0]); - - var cmd1 = new RemoveMenuItemOperation(topChild); - ClassicAssert.IsTrue(cmd1.Do()); - - var cmd2 = new RemoveMenuItemOperation(bottomChild); - ClassicAssert.IsTrue(cmd2.Do()); - - // Deleting both children should convert us from - // a dropdown submenu to just a regular MenuItem - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(typeof(MenuItem), bar.Menus[0].Children[1].GetType()); - - cmd2.Undo(); - - // should bring the bottom one back - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(typeof(MenuBarItem), bar.Menus[0].Children[1].GetType()); - ClassicAssert.AreSame(bottomChild, ((MenuBarItem)bar.Menus[0].Children[1]).Children[0]); - - cmd1.Undo(); - - // Both submenu items should now be back - ClassicAssert.AreEqual(3, bar.Menus[0].Children.Length); - ClassicAssert.AreEqual(typeof(MenuBarItem), bar.Menus[0].Children[1].GetType()); - ClassicAssert.AreSame(topChild, ((MenuBarItem)bar.Menus[0].Children[1]).Children[0]); - ClassicAssert.AreSame(bottomChild, ((MenuBarItem)bar.Menus[0].Children[1]).Children[1]); - } - - [Test] - public void TestDeletingLastMenuItem_ShouldRemoveWholeBar() - { - var bar = this.GetMenuBar(out Design root); - - var mi = bar.Menus[0].Children[0]; - - ClassicAssert.Contains(bar, root.View.Subviews.ToArray(), - "The MenuBar should be on the main view being edited"); - - var cmd = new RemoveMenuItemOperation(mi); - ClassicAssert.IsTrue(cmd.Do()); - - ClassicAssert.IsEmpty(bar.Menus, "Expected menu bar header (File) to be removed along with it's last (only) child"); - - ClassicAssert.IsFalse( - root.View.Subviews.Contains(bar), - "Now that the MenuBar is completely empty it should be automatically removed"); - - cmd.Undo(); - - ClassicAssert.Contains(bar, root.View.Subviews.ToArray(), - "Undo should put the MenuBar back on the view again"); + /// + public void Dispose( ) + { + Bar.Dispose( ); + } } } \ No newline at end of file diff --git a/tests/UnitTests.csproj b/tests/UnitTests.csproj index e4655727..4c2cb88e 100644 --- a/tests/UnitTests.csproj +++ b/tests/UnitTests.csproj @@ -29,7 +29,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive