using System; using UnitTestSharp; namespace Glass.UnitTests { public class KeyboardManagerTests : TestFixture { public static Control Focusable(string debugName = null) { return new Control(debugName) { Focusable = true }; } /// /// Calls back into from a getter the tab walk reads. /// public class ReentrantControl : Control { public KeyboardManager Manager { get; set; } public override bool Enabled { get { Manager?.MoveToNextFocusable(forward: true); return base.Enabled; } set => base.Enabled = value; } } public class CtorTests : TestFixture { public void Default_MakesItsOwnControlTree() { CheckNotNull(new KeyboardManager().Controls); } public void Default_MakesAContainerPerManager() { CheckNotEqual(new KeyboardManager().Controls, new KeyboardManager().Controls); } public void GivenAContainer_TracksThatOne() { var controls = new ControlContainer(); CheckEqual(controls, new KeyboardManager(controls).Controls); } } public class FocusedTests : TestFixture { public void ByDefault_IsNull() { CheckNull(new KeyboardManager().Focused); } public void AfterTrySetFocusSucceeds_ReturnsThatControl() { var manager = new KeyboardManager(); var control = Focusable(); manager.TrySetFocus(control); CheckEqual(control, manager.Focused); } public void AfterTheFocusedControlIsDisabled_ReturnsNull() { var manager = new KeyboardManager(); var control = Focusable(); manager.TrySetFocus(control); control.Enabled = false; CheckNull(manager.Focused); } public void AfterTheFocusedControlIsHidden_ReturnsNull() { var manager = new KeyboardManager(); var control = Focusable(); manager.TrySetFocus(control); control.Visible = false; CheckNull(manager.Focused); } public void AfterTheFocusedControlIsRemovedFromItsContainer_ReturnsNull() { var controls = new ControlContainer(); var manager = new KeyboardManager(controls); var control = Focusable(); controls.Add(control); manager.TrySetFocus(control); controls.Remove(control); CheckNull(manager.Focused); } public void AfterClearFocus_ReturnsNull() { var manager = new KeyboardManager(); manager.TrySetFocus(Focusable()); manager.ClearFocus(); CheckNull(manager.Focused); } } public class TrySetFocusTests : TestFixture { public void FocusableEnabledVisibleControl_Succeeds() { CheckTrue(new KeyboardManager().TrySetFocus(Focusable())); } public void NullControl_Fails() { CheckFalse(new KeyboardManager().TrySetFocus(null)); } public void NonFocusableControl_Fails() { CheckFalse(new KeyboardManager().TrySetFocus(new Control())); } public void DisabledControl_Fails() { var control = Focusable(); control.Enabled = false; CheckFalse(new KeyboardManager().TrySetFocus(control)); } public void HiddenControl_Fails() { var control = Focusable(); control.Visible = false; CheckFalse(new KeyboardManager().TrySetFocus(control)); } public void Failing_LeavesAnyExistingFocusAlone() { var manager = new KeyboardManager(); var alreadyFocused = Focusable(); manager.TrySetFocus(alreadyFocused); manager.TrySetFocus(new Control()); CheckEqual(alreadyFocused, manager.Focused); } public void Failing_DoesNotCancelTheExistingFocus() { var manager = new KeyboardManager(); var alreadyFocused = new ControlTests.CancelCountingControl { Focusable = true }; manager.TrySetFocus(alreadyFocused); manager.TrySetFocus(new Control()); CheckEqual(0, alreadyFocused.FocusCancelCount); } public void TakingFocusFromAnotherControl_CancelsItsFocus() { var manager = new KeyboardManager(); var outgoing = new ControlTests.CancelCountingControl { Focusable = true }; var incoming = new ControlTests.CancelCountingControl { Focusable = true }; manager.TrySetFocus(outgoing); manager.TrySetFocus(incoming); CheckEqual(1, outgoing.FocusCancelCount); CheckEqual(0, incoming.FocusCancelCount); } public void ReaffirmingTheSameControl_DoesNotCancelItsOwnFocus() { var manager = new KeyboardManager(); var control = new ControlTests.CancelCountingControl { Focusable = true }; manager.TrySetFocus(control); manager.TrySetFocus(control); CheckEqual(0, control.FocusCancelCount); CheckEqual(control, manager.Focused); } public void Succeeding_ForgetsAnyEscapedControl() { var manager = new KeyboardManager(); var other = Focusable("other"); var escaped = Focusable("escaped"); manager.Controls.Add(other); manager.Controls.Add(escaped); manager.TrySetFocus(escaped); manager.KeyDown(KeyCode.Escape, default); manager.TrySetFocus(other); manager.ClearFocus(); // Had the escape memory survived, this would land back on escaped instead of tree order's first. CheckEqual(other, manager.MoveToNextFocusable(forward: true)); } } public class ClearFocusTests : TestFixture { public void Always_MakesFocusedNull() { var manager = new KeyboardManager(); manager.TrySetFocus(Focusable()); manager.ClearFocus(); CheckNull(manager.Focused); } public void Always_CancelsTheOutgoingControlsFocus() { var manager = new KeyboardManager(); var control = new ControlTests.CancelCountingControl { Focusable = true }; manager.TrySetFocus(control); manager.ClearFocus(); CheckEqual(1, control.FocusCancelCount); } public void WithNothingFocused_DoesNotThrow() { new KeyboardManager().ClearFocus(); } public void AfterEscape_ForgetsTheEscapedControlToo() { var manager = new KeyboardManager(); var other = Focusable("other"); var escaped = Focusable("escaped"); manager.Controls.Add(other); manager.Controls.Add(escaped); manager.TrySetFocus(escaped); manager.KeyDown(KeyCode.Escape, default); // A blur that isn't Escape (eg a click on empty space) forfeits the "resume it" courtesy. manager.ClearFocus(); CheckEqual(other, manager.MoveToNextFocusable(forward: true)); } } public class FocusFromClickTests : TestFixture { public void ControlThatTakesFocusOnClick_FocusesIt() { var manager = new KeyboardManager(); var control = Focusable(); control.FocusOnClick = true; manager.FocusFromClick(control); CheckEqual(control, manager.Focused); } public void ControlThatTakesFocusOnClick_TakesItFromWhateverHadIt() { var manager = new KeyboardManager(); var previous = Focusable("previous"); var control = Focusable("control"); control.FocusOnClick = true; manager.TrySetFocus(previous); manager.FocusFromClick(control); CheckEqual(control, manager.Focused); } public void ControlThatDoesNotTakeFocusOnClick_LeavesFocusAlone() { var manager = new KeyboardManager(); var focused = Focusable("focused"); manager.TrySetFocus(focused); manager.FocusFromClick(Focusable("button")); CheckEqual(focused, manager.Focused); } public void ControlThatTakesFocusOnClickButIsNotFocusable_LeavesFocusAlone() { var manager = new KeyboardManager(); var focused = Focusable("focused"); manager.TrySetFocus(focused); manager.FocusFromClick(new Control { FocusOnClick = true }); CheckEqual(focused, manager.Focused); } public void ControlThatTakesFocusOnClickButIsDisabled_LeavesFocusAlone() { var manager = new KeyboardManager(); var focused = Focusable("focused"); manager.TrySetFocus(focused); var disabled = Focusable("disabled"); disabled.FocusOnClick = true; disabled.Enabled = false; manager.FocusFromClick(disabled); CheckEqual(focused, manager.Focused); } public void NothingClaimedThePress_ClearsFocus() { var manager = new KeyboardManager(); manager.TrySetFocus(Focusable()); manager.FocusFromClick(null); CheckNull(manager.Focused); } public void NothingClaimedThePress_WithNothingFocused_LeavesNothingFocused() { var manager = new KeyboardManager(); manager.FocusFromClick(null); CheckNull(manager.Focused); } } public class MoveToNextFocusableTests : TestFixture { public void NoFocusableControls_ReturnsNull() { var manager = new KeyboardManager(); manager.Controls.Add(new Control()); CheckNull(manager.MoveToNextFocusable(forward: true)); } public void NoFocusableControls_ClearsAnyExistingFocus() { var controls = new ControlContainer(); var manager = new KeyboardManager(controls); var control = Focusable(); controls.Add(control); manager.TrySetFocus(control); controls.Remove(control); controls.Add(new Control()); manager.MoveToNextFocusable(forward: true); CheckNull(manager.Focused); } public void NothingFocused_Forward_FocusesTheFirstOne() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); manager.Controls.Add(first); manager.Controls.Add(second); CheckEqual(first, manager.MoveToNextFocusable(forward: true)); } public void NothingFocused_Backward_FocusesTheLastOne() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); manager.Controls.Add(first); manager.Controls.Add(second); CheckEqual(second, manager.MoveToNextFocusable(forward: false)); } public void MovingToAnotherControl_CancelsThePreviousOnesFocus() { var manager = new KeyboardManager(); var first = new ControlTests.CancelCountingControl { Focusable = true }; var second = Focusable("second"); manager.Controls.Add(first); manager.Controls.Add(second); manager.TrySetFocus(first); manager.MoveToNextFocusable(forward: true); CheckEqual(1, first.FocusCancelCount); } public void MiddleFocused_Forward_MovesToTheNextOne() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); var third = Focusable("third"); manager.Controls.Add(first); manager.Controls.Add(second); manager.Controls.Add(third); manager.TrySetFocus(second); CheckEqual(third, manager.MoveToNextFocusable(forward: true)); } public void MiddleFocused_Backward_MovesToThePreviousOne() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); var third = Focusable("third"); manager.Controls.Add(first); manager.Controls.Add(second); manager.Controls.Add(third); manager.TrySetFocus(second); CheckEqual(first, manager.MoveToNextFocusable(forward: false)); } public void LastFocused_Forward_WrapsToTheFirstOne() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); manager.Controls.Add(first); manager.Controls.Add(second); manager.TrySetFocus(second); CheckEqual(first, manager.MoveToNextFocusable(forward: true)); } public void FirstFocused_Backward_WrapsToTheLastOne() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); manager.Controls.Add(first); manager.Controls.Add(second); manager.TrySetFocus(first); CheckEqual(second, manager.MoveToNextFocusable(forward: false)); } public void SingleFocusableControl_WrapsToItself() { var manager = new KeyboardManager(); var only = Focusable(); manager.Controls.Add(only); manager.TrySetFocus(only); CheckEqual(only, manager.MoveToNextFocusable(forward: true)); } public void SkipsNonFocusableControls() { var manager = new KeyboardManager(); var first = Focusable("first"); var third = Focusable("third"); manager.Controls.Add(first); manager.Controls.Add(new Control("second")); manager.Controls.Add(third); manager.TrySetFocus(first); CheckEqual(third, manager.MoveToNextFocusable(forward: true)); } public void SkipsDisabledControls() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); second.Enabled = false; var third = Focusable("third"); manager.Controls.Add(first); manager.Controls.Add(second); manager.Controls.Add(third); manager.TrySetFocus(first); CheckEqual(third, manager.MoveToNextFocusable(forward: true)); } public void SkipsHiddenControls() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); second.Visible = false; var third = Focusable("third"); manager.Controls.Add(first); manager.Controls.Add(second); manager.Controls.Add(third); manager.TrySetFocus(first); CheckEqual(third, manager.MoveToNextFocusable(forward: true)); } public void VisitsAFocusableParentBeforeItsFocusableChildren() { var manager = new KeyboardManager(); var parent = Focusable("parent"); var child = Focusable("child"); parent.CanvasChildren.Add(child); manager.Controls.Add(parent); CheckEqual(parent, manager.MoveToNextFocusable(forward: true)); CheckEqual(child, manager.MoveToNextFocusable(forward: true)); } public void CyclicTree_Throws() { var manager = new KeyboardManager(); var parent = Focusable("parent"); var child = Focusable("child"); parent.CanvasChildren.Add(child); child.CanvasChildren.Add(parent); manager.Controls.Add(parent); CheckThrow(typeof(InvalidOperationException)); manager.MoveToNextFocusable(forward: true); } public void ControlUsedTwice_Throws() { var manager = new KeyboardManager(); var control = Focusable(); manager.Controls.Add(control); manager.Controls.Add(control); CheckThrow(typeof(InvalidOperationException)); manager.MoveToNextFocusable(forward: true); } public void CalledFromInsideTheWalk_Throws() { var manager = new KeyboardManager(); manager.Controls.Add(new ReentrantControl { Manager = manager }); CheckThrow(typeof(InvalidOperationException)); manager.MoveToNextFocusable(forward: true); } public void AfterAWalkThrows_TheNextOneStillWorks() { var manager = new KeyboardManager(); var reentrant = new ReentrantControl { Manager = manager }; var focusable = Focusable(); manager.Controls.Add(reentrant); manager.Controls.Add(focusable); try { manager.MoveToNextFocusable(forward: true); } catch (InvalidOperationException) { } reentrant.Manager = null; CheckEqual(focusable, manager.MoveToNextFocusable(forward: true)); } public void AfterEscape_Forward_ResumesTheEscapedControl() { var manager = new KeyboardManager(); var first = Focusable("first"); var escaped = Focusable("escaped"); manager.Controls.Add(first); manager.Controls.Add(escaped); manager.TrySetFocus(escaped); manager.KeyDown(KeyCode.Escape, default); CheckEqual(escaped, manager.MoveToNextFocusable(forward: true)); } public void AfterEscape_Backward_AlsoResumesTheEscapedControl() { var manager = new KeyboardManager(); var first = Focusable("first"); var escaped = Focusable("escaped"); manager.Controls.Add(first); manager.Controls.Add(escaped); manager.TrySetFocus(escaped); manager.KeyDown(KeyCode.Escape, default); CheckEqual(escaped, manager.MoveToNextFocusable(forward: false)); } public void AfterEscape_ResumingItAgainMovesToTheNextOneInstead() { var manager = new KeyboardManager(); var escaped = Focusable("escaped"); var next = Focusable("next"); manager.Controls.Add(escaped); manager.Controls.Add(next); manager.TrySetFocus(escaped); manager.KeyDown(KeyCode.Escape, default); manager.MoveToNextFocusable(forward: true); CheckEqual(next, manager.MoveToNextFocusable(forward: true)); } public void AfterEscapeThenTheEscapedControlIsDisabled_FallsBackToOrdinaryOrder() { var manager = new KeyboardManager(); var first = Focusable("first"); var escaped = Focusable("escaped"); manager.Controls.Add(first); manager.Controls.Add(escaped); manager.TrySetFocus(escaped); manager.KeyDown(KeyCode.Escape, default); escaped.Enabled = false; CheckEqual(first, manager.MoveToNextFocusable(forward: true)); } public void AfterAWalkThrows_LeavesNothingBehindForTheNextOne() { var controls = new ControlContainer(); var manager = new KeyboardManager(controls); var reentrant = new ReentrantControl { Manager = manager }; var abandoned = Focusable("abandoned"); controls.Add(reentrant); controls.Add(abandoned); try { manager.MoveToNextFocusable(forward: true); } catch (InvalidOperationException) { } reentrant.Manager = null; controls.Remove(abandoned); var replacement = Focusable("replacement"); controls.Add(replacement); CheckEqual(replacement, manager.MoveToNextFocusable(forward: true)); } } public class KeyDownTests : TestFixture { public void Tab_MovesFocusForward() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); manager.Controls.Add(first); manager.Controls.Add(second); manager.TrySetFocus(first); manager.KeyDown(KeyCode.Tab, default); CheckEqual(second, manager.Focused); } public void Tab_WithSomethingFocusable_Claims() { var manager = new KeyboardManager(); manager.Controls.Add(Focusable()); CheckTrue(manager.KeyDown(KeyCode.Tab, default)); } public void Tab_WithNothingFocusable_Declines() { var manager = new KeyboardManager(); manager.Controls.Add(new Control()); CheckFalse(manager.KeyDown(KeyCode.Tab, default)); } public void ShiftTab_MovesFocusBackward() { var manager = new KeyboardManager(); var first = Focusable("first"); var second = Focusable("second"); manager.Controls.Add(first); manager.Controls.Add(second); manager.TrySetFocus(second); manager.KeyDown(KeyCode.Tab, new KeyState(KeyCode.ShiftLeft)); CheckEqual(first, manager.Focused); } public void Escape_ClearsFocusAndClaims() { var manager = new KeyboardManager(); manager.TrySetFocus(Focusable()); CheckTrue(manager.KeyDown(KeyCode.Escape, default)); CheckNull(manager.Focused); } public void Escape_WithNothingFocused_Declines() { CheckFalse(new KeyboardManager().KeyDown(KeyCode.Escape, default)); } public void Escape_CancelsTheFocusedControlsFocus() { var manager = new KeyboardManager(); var control = new ControlTests.CancelCountingControl { Focusable = true }; manager.TrySetFocus(control); manager.KeyDown(KeyCode.Escape, default); CheckEqual(1, control.FocusCancelCount); } public void EscapeThenTab_RefocusesTheEscapedControl() { var manager = new KeyboardManager(); var first = Focusable("first"); var escaped = Focusable("escaped"); manager.Controls.Add(first); manager.Controls.Add(escaped); manager.TrySetFocus(escaped); manager.KeyDown(KeyCode.Escape, default); manager.KeyDown(KeyCode.Tab, default); CheckEqual(escaped, manager.Focused); } public void OtherKey_Declines() { var manager = new KeyboardManager(); manager.TrySetFocus(Focusable()); CheckFalse(manager.KeyDown(KeyCode.Space, default)); } } } }