namespace ViewBaseTests.Navigation; public class AdvanceFocusTests () { [Fact] public void AdvanceFocus_CanFocus_Mixed () { var r = new View (); var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; var v2 = new View { CanFocus = false, TabStop = TabBehavior.TabStop }; var v3 = new View { CanFocus = false, TabStop = TabBehavior.NoStop }; r.Add (v1, v2, v3); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.Dispose (); } [Theory] [CombinatorialData] public void AdvanceFocus_Change_CanFocus_Works ([CombinatorialValues (TabBehavior.NoStop, TabBehavior.TabStop, TabBehavior.TabGroup)] TabBehavior behavior) { var r = new View { CanFocus = true }; var v1 = new View (); var v2 = new View (); var v3 = new View (); Assert.True (r.CanFocus); Assert.False (v1.CanFocus); Assert.False (v2.CanFocus); Assert.False (v3.CanFocus); r.Add (v1, v2, v3); r.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); v1.CanFocus = true; v1.TabStop = behavior; r.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); v2.CanFocus = true; v2.TabStop = behavior; r.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.False (v1.HasFocus); Assert.True (v2.HasFocus); Assert.False (v3.HasFocus); v3.CanFocus = true; v3.TabStop = behavior; r.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.True (v3.HasFocus); r.Dispose (); } [Fact] public void AdvanceFocus_SubViews_TabStop () { TabBehavior behavior = TabBehavior.TabStop; var top = new View { Id = "top", CanFocus = true }; var v1 = new View { Id = "v1", CanFocus = true, TabStop = behavior }; var v2 = new View { Id = "v2", CanFocus = true, TabStop = behavior }; var v3 = new View { Id = "v3", CanFocus = false, TabStop = behavior }; top.Add (v1, v2, v3); // Cycle through v1 & v2 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v1.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v2.HasFocus); // Should cycle back to v1 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v1.HasFocus); // Go backwards top.AdvanceFocus (NavigationDirection.Backward, behavior); Assert.True (v2.HasFocus); top.AdvanceFocus (NavigationDirection.Backward, behavior); Assert.True (v1.HasFocus); top.Dispose (); } [Fact] public void AdvanceFocus_Compound_SubView_TabStop () { TabBehavior behavior = TabBehavior.TabStop; var top = new View { Id = "top", CanFocus = true }; var compoundSubView = new View { CanFocus = true, Id = "compoundSubView", TabStop = behavior }; var v1 = new View { Id = "v1", CanFocus = true, TabStop = behavior }; var v2 = new View { Id = "v2", CanFocus = true, TabStop = behavior }; var v3 = new View { Id = "v3", CanFocus = false, TabStop = behavior }; compoundSubView.Add (v1, v2, v3); top.Add (compoundSubView); // Cycle through v1 & v2 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.Equal (v1, compoundSubView.Focused); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.Equal (v2, compoundSubView.Focused); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.Equal (v1, compoundSubView.Focused); // Add another subview View otherSubView = new () { CanFocus = true, TabStop = behavior, Id = "otherSubView" }; top.Add (otherSubView); // Adding a focusable subview causes advancefocus Assert.True (otherSubView.HasFocus); Assert.False (v1.HasFocus); // Cycle through v1 & v2 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.False (v1.HasFocus); Assert.True (v2.HasFocus); Assert.False (v3.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); Assert.True (otherSubView.HasFocus); // v2 was previously focused down the compoundSubView focus chain top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.False (v1.HasFocus); Assert.True (v2.HasFocus); Assert.False (v3.HasFocus); top.Dispose (); } [Fact] public void AdvanceFocus_CompoundCompound_SubView_TabStop () { //top // ├── topv1, topv2, topv3 (only topv1 and topv2 are focusable) // └── compoundSubView // ├── v1, v2, v3 (v3 is not focusable) // └── compoundCompoundSubView // └── v4, v5, v6 (v6 is not focusable) TabBehavior behavior = TabBehavior.TabStop; var top = new View { Id = "top", CanFocus = true }; var topv1 = new View { Id = "topv1", CanFocus = true, TabStop = behavior }; var topv2 = new View { Id = "topv2", CanFocus = true, TabStop = behavior }; var topv3 = new View { Id = "topv3", CanFocus = false, TabStop = behavior }; top.Add (topv1, topv2, topv3); var compoundSubView = new View { CanFocus = true, Id = "compoundSubView", TabStop = behavior }; var v1 = new View { Id = "v1", CanFocus = true, TabStop = behavior }; var v2 = new View { Id = "v2", CanFocus = true, TabStop = behavior }; var v3 = new View { Id = "v3", CanFocus = false, TabStop = behavior }; compoundSubView.Add (v1, v2, v3); var compoundCompoundSubView = new View { CanFocus = true, Id = "compoundCompoundSubView", TabStop = behavior }; var v4 = new View { Id = "v4", CanFocus = true, TabStop = behavior }; var v5 = new View { Id = "v5", CanFocus = true, TabStop = behavior }; var v6 = new View { Id = "v6", CanFocus = false, TabStop = behavior }; compoundCompoundSubView.Add (v4, v5, v6); compoundSubView.Add (compoundCompoundSubView); top.Add (compoundSubView); top.SetFocus (); Assert.True (topv1.HasFocus); // Cycle through topv2 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (topv2.HasFocus); // Cycle v1, v2, v4, v5 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v1.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v2.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v4.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (v5.HasFocus); // Should cycle back to topv1 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.Equal (topv1, top.Focused); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.Equal (topv2, top.Focused); // Add another top subview. Should cycle to it after v5 View otherSubView = new () { CanFocus = true, TabStop = behavior, Id = "otherSubView" }; top.Add (otherSubView); // Adding a focusable subview causes advancefocus Assert.True (otherSubView.HasFocus); // Cycle through topv1, topv2, v1, v2, v4, v5 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (topv1.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (topv2.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); // the above should have cycled to v5 since it was the previously most focused subview of compoundSubView Assert.True (v5.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (otherSubView.HasFocus); // Should cycle back to topv1 top.AdvanceFocus (NavigationDirection.Forward, behavior); Assert.True (topv1.HasFocus); top.Dispose (); } [Fact] public void FocusNavigation_Should_Cycle_Back_To_Top_Level_Views () { // Create a simplified version of the hierarchy from CompoundCompound test // top // ├── topButton1, topButton2 // └── nestedContainer // └── innerButton // Create top-level view View top = new () { Id = "top", CanFocus = true }; // Create top-level buttons View topButton1 = new () { Id = "topButton1", CanFocus = true, TabStop = TabBehavior.TabStop }; View topButton2 = new () { Id = "topButton2", CanFocus = true, TabStop = TabBehavior.TabStop }; // Create nested container with a button inside View nestedContainer = new () { Id = "nestedContainer", CanFocus = true, TabStop = TabBehavior.TabStop }; View innerButton = new () { Id = "innerButton", CanFocus = true, TabStop = TabBehavior.TabStop }; // Build the view hierarchy nestedContainer.Add (innerButton); top.Add (topButton1, topButton2, nestedContainer); // Initial focus on topButton1 topButton1.SetFocus (); Assert.Equal (topButton1, top.Focused); // Advance focus to topButton2 top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.Equal (topButton2, top.Focused); // Advance focus to innerButton top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.Equal (innerButton, nestedContainer.Focused); Assert.Equal (nestedContainer, top.Focused); // THIS IS WHERE THE BUG OCCURS // Advancing focus from innerButton should go back to topButton1 top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); // This assertion will fail with current implementation Assert.Equal (topButton1, top.Focused); top.Dispose (); } [Fact] public void AdvanceFocus_Compound_SubView_TabGroup () { var top = new View { Id = "top", CanFocus = true, TabStop = TabBehavior.TabGroup }; var compoundSubView = new View { CanFocus = true, Id = "compoundSubView", TabStop = TabBehavior.TabGroup }; var tabStopView = new View { Id = "tabStop", CanFocus = true, TabStop = TabBehavior.TabStop }; var tabGroupView1 = new View { Id = "tabGroup1", CanFocus = true, TabStop = TabBehavior.TabGroup }; var tabGroupView2 = new View { Id = "tabGroup2", CanFocus = true, TabStop = TabBehavior.TabGroup }; compoundSubView.Add (tabStopView, tabGroupView1, tabGroupView2); top.Add (compoundSubView); top.SetFocus (); Assert.True (tabStopView.HasFocus); // TabGroup should cycle to tabGroup1 then tabGroup2 top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); Assert.False (tabStopView.HasFocus); Assert.True (tabGroupView1.HasFocus); Assert.False (tabGroupView2.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); Assert.False (tabStopView.HasFocus); Assert.False (tabGroupView1.HasFocus); Assert.True (tabGroupView2.HasFocus); // Add another TabGroup subview View otherTabGroupSubView = new () { CanFocus = true, TabStop = TabBehavior.TabGroup, Id = "otherTabGroupSubView" }; top.Add (otherTabGroupSubView); // Adding a focusable subview causes advancefocus Assert.True (otherTabGroupSubView.HasFocus); Assert.False (tabStopView.HasFocus); // TabGroup navs to the other subview top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); Assert.Equal (compoundSubView, top.Focused); Assert.True (tabStopView.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); Assert.Equal (compoundSubView, top.Focused); Assert.True (tabGroupView1.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); Assert.Equal (compoundSubView, top.Focused); Assert.True (tabGroupView2.HasFocus); // Now go backwards top.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup); Assert.Equal (compoundSubView, top.Focused); Assert.True (tabGroupView1.HasFocus); top.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup); Assert.Equal (otherTabGroupSubView, top.Focused); Assert.True (otherTabGroupSubView.HasFocus); top.Dispose (); } [Fact] public void AdvanceFocus_NoStop_And_CanFocus_True_No_Focus () { var r = new View (); var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; r.Add (v1, v2, v3); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.Dispose (); } [Fact] public void AdvanceFocus_NoStop_Change_Enables_Stop () { var r = new View { CanFocus = true }; var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; r.Add (v1, v2, v3); v1.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.True (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); v2.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.True (v2.HasFocus); Assert.False (v3.HasFocus); v3.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.True (v3.HasFocus); r.Dispose (); } [Fact] public void AdvanceFocus_NoStop_Prevents_Stop () { var r = new View (); var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; r.Add (v1, v2, v3); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); } [Fact] public void AdvanceFocus_Null_And_CanFocus_False_No_Advance () { var r = new View (); var v1 = new View (); var v2 = new View (); var v3 = new View (); Assert.False (v1.CanFocus); Assert.Null (v1.TabStop); r.Add (v1, v2, v3); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); r.Dispose (); } [Fact] public void AdvanceFocus_SubViews_Raises_HasFocusChanged () { var top = new View { Id = "top", CanFocus = true }; var subView1 = new View { Id = "subView1", CanFocus = true }; var subView2 = new View { Id = "subView2", CanFocus = true }; top.Add (subView1, subView2); var subView1HasFocusChangedTrueCount = 0; var subView1HasFocusChangedFalseCount = 0; subView1.HasFocusChanged += (s, e) => { if (e.NewValue) { subView1HasFocusChangedTrueCount++; } else { subView1HasFocusChangedFalseCount++; } }; var subView2HasFocusChangedTrueCount = 0; var subView2HasFocusChangedFalseCount = 0; subView2.HasFocusChanged += (s, e) => { if (e.NewValue) { subView2HasFocusChangedTrueCount++; } else { subView2HasFocusChangedFalseCount++; } }; top.SetFocus (); Assert.True (top.HasFocus); Assert.True (subView1.HasFocus); Assert.False (subView2.HasFocus); Assert.Equal (1, subView1HasFocusChangedTrueCount); Assert.Equal (0, subView1HasFocusChangedFalseCount); Assert.Equal (0, subView2HasFocusChangedTrueCount); Assert.Equal (0, subView2HasFocusChangedFalseCount); top.AdvanceFocus (NavigationDirection.Forward, null); Assert.False (subView1.HasFocus); Assert.True (subView2.HasFocus); Assert.Equal (1, subView1HasFocusChangedTrueCount); Assert.Equal (1, subView1HasFocusChangedFalseCount); Assert.Equal (1, subView2HasFocusChangedTrueCount); Assert.Equal (0, subView2HasFocusChangedFalseCount); top.AdvanceFocus (NavigationDirection.Forward, null); Assert.True (subView1.HasFocus); Assert.False (subView2.HasFocus); Assert.Equal (2, subView1HasFocusChangedTrueCount); Assert.Equal (1, subView1HasFocusChangedFalseCount); Assert.Equal (1, subView2HasFocusChangedTrueCount); Assert.Equal (1, subView2HasFocusChangedFalseCount); } [Fact] public void AdvanceFocus_SubViews_Raises_HasFocusChanging () { var top = new View { Id = "top", CanFocus = true }; var subView1 = new View { Id = "subView1", CanFocus = true }; var subView2 = new View { Id = "subView2", CanFocus = true }; top.Add (subView1, subView2); var subView1HasFocusChangingTrueCount = 0; var subView1HasFocusChangingFalseCount = 0; subView1.HasFocusChanging += (s, e) => { if (e.NewValue) { subView1HasFocusChangingTrueCount++; } else { subView1HasFocusChangingFalseCount++; } }; var subView2HasFocusChangingTrueCount = 0; var subView2HasFocusChangingFalseCount = 0; subView2.HasFocusChanging += (s, e) => { if (e.NewValue) { subView2HasFocusChangingTrueCount++; } else { subView2HasFocusChangingFalseCount++; } }; top.SetFocus (); Assert.True (top.HasFocus); Assert.True (subView1.HasFocus); Assert.False (subView2.HasFocus); Assert.Equal (1, subView1HasFocusChangingTrueCount); Assert.Equal (0, subView1HasFocusChangingFalseCount); Assert.Equal (0, subView2HasFocusChangingTrueCount); Assert.Equal (0, subView2HasFocusChangingFalseCount); top.AdvanceFocus (NavigationDirection.Forward, null); Assert.False (subView1.HasFocus); Assert.True (subView2.HasFocus); Assert.Equal (1, subView1HasFocusChangingTrueCount); Assert.Equal (1, subView1HasFocusChangingFalseCount); Assert.Equal (1, subView2HasFocusChangingTrueCount); Assert.Equal (0, subView2HasFocusChangingFalseCount); top.AdvanceFocus (NavigationDirection.Forward, null); Assert.True (subView1.HasFocus); Assert.False (subView2.HasFocus); Assert.Equal (2, subView1HasFocusChangingTrueCount); Assert.Equal (1, subView1HasFocusChangingFalseCount); Assert.Equal (1, subView2HasFocusChangingTrueCount); Assert.Equal (1, subView2HasFocusChangingFalseCount); } [Fact] public void AdvanceFocus_With_CanFocus_Are_All_True () { var top = new View { Id = "top", CanFocus = true }; var v1 = new View { Id = "v1", CanFocus = true }; var v2 = new View { Id = "v2", CanFocus = true }; var v3 = new View { Id = "v3", CanFocus = true }; top.Add (v1, v2, v3); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.True (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.True (v2.HasFocus); Assert.False (v3.HasFocus); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); Assert.True (v3.HasFocus); top.Dispose (); } [Theory] [CombinatorialData] public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabBehavior tabStop) { var view = new View { CanFocus = canFocus, TabStop = tabStop }; Assert.Equal (canFocus, view.CanFocus); Assert.Equal (tabStop, view.TabStop); } [Fact] public void AdvanceFocus_Cycles_Through_Peers_And_All_Nested_SubViews_When_Multiple () { var top = new View { Id = "top", CanFocus = true }; View peer1 = new View { CanFocus = true, Id = "peer1", }; var peer2 = new View { CanFocus = true, Id = "peer2", }; var peer2SubView = new View { Id = "peer2SubView", CanFocus = true }; var v1 = new View { Id = "v1", CanFocus = true }; var v2 = new View { Id = "v2", CanFocus = true }; peer2SubView.Add (v1, v2); peer2.Add (peer2SubView); top.Add (peer1, peer2); top.SetFocus (); Assert.Equal (peer1, top.MostFocused); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.Equal (v1, top.MostFocused); top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.Equal (v2, top.MostFocused); // This should cycle to peer1 - previously it incorrectly cycled to v1 top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Assert.Equal (peer1, top.MostFocused); top.Dispose (); } }