From 33ed34b53285d6eeb751ecb866cb96d932052915 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Mon, 17 Apr 2023 11:46:28 +1200 Subject: [PATCH] Add more UI helper methods for tests (#15463) --- .../Tests/Chemistry/DispenserTest.cs | 14 +- .../Interaction/InteractionTest.Helpers.cs | 176 ++++++++++++++++-- .../Tests/Interaction/InteractionTest.cs | 5 +- 3 files changed, 178 insertions(+), 17 deletions(-) diff --git a/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs b/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs index cbe3ee4dce..3978dfb100 100644 --- a/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs +++ b/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Content.Client.Chemistry.UI; using Content.IntegrationTests.Tests.Interaction; using Content.Shared.Chemistry; using Content.Shared.Containers.ItemSlots; @@ -22,7 +23,7 @@ public sealed class DispenserTest : InteractionTest Assert.IsNull(Hands.ActiveHandEntity); // Open BUI - await Interact(""); + await Interact(); // Eject beaker via BUI. var ev = new ItemSlotButtonPressedEvent(SharedChemMaster.InputSlotName); @@ -31,5 +32,16 @@ public sealed class DispenserTest : InteractionTest // Beaker is back in the player's hands Assert.IsNotNull(Hands.ActiveHandEntity); AssertPrototype("Beaker", Hands.ActiveHandEntity); + + // Re-insert the beaker + await Interact(); + Assert.IsNull(Hands.ActiveHandEntity); + + // Re-eject using the button directly instead of sending a BUI event. This test is really just a test of the + // bui/window helper methods. + await ClickControl(nameof(ReagentDispenserWindow.EjectButton)); + await RunTicks(5); + Assert.IsNotNull(Hands.ActiveHandEntity); + AssertPrototype("Beaker", Hands.ActiveHandEntity); } } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index 634755062b..e945efe3dc 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Threading.Tasks; +using Content.Client.Chemistry.UI; using Content.Client.Construction; using Content.Server.Construction.Components; using Content.Server.Power.Components; @@ -11,8 +14,13 @@ using Content.Server.Tools.Components; using Content.Shared.Construction.Prototypes; using Content.Shared.Item; using NUnit.Framework; +using OpenToolkit.GraphicsLibraryFramework; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; using Robust.Shared.GameObjects; +using Robust.Shared.Input; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -221,6 +229,8 @@ public abstract partial class InteractionTest Assert.IsNull(Hands.ActiveHandEntity); } + #region Interact + /// /// Use the currently held entity. /// @@ -263,7 +273,14 @@ public abstract partial class InteractionTest } await PlaceInHands(entity); + await Interact(shouldSucceed, awaitDoAfters); + } + /// + /// Interact with an entity using the currently held entity. + /// + protected async Task Interact(bool shouldSucceed = true, bool awaitDoAfters = true) + { if (Target == null || !Target.Value.IsClientSide()) { await Server.WaitPost(() => InteractSys.UserInteraction(Player, TargetCoords, Target)); @@ -283,6 +300,22 @@ public abstract partial class InteractionTest await CheckTargetChange(shouldSucceed && awaitDoAfters); } + /// + /// Variant of that performs several interactions using different entities. + /// + /// + /// Empty strings imply empty hands. + /// + protected async Task Interact(params EntitySpecifier[] specifiers) + { + foreach (var spec in specifiers) + { + await Interact(spec); + } + } + + #endregion + /// /// Wait for any currently active DoAfters to finish. /// @@ -382,20 +415,6 @@ public abstract partial class InteractionTest await CheckTargetChange(shouldSucceed); } - /// - /// Variant of that performs several interactions using different entities. - /// - /// - /// Empty strings imply empty hands. - /// - protected async Task Interact(params EntitySpecifier[] specifiers) - { - foreach (var spec in specifiers) - { - await Interact(spec); - } - } - #region Asserts protected void AssertPrototype(string? prototype, EntityUid? target = null) @@ -721,6 +740,135 @@ public abstract partial class InteractionTest #endregion + #region UI + + /// + /// Presses and releases a button on some client-side window. Will fail if the button cannot be found. + /// + protected async Task ClickControl(string name) where TWindow : BaseWindow + { + await ClickControl(GetControl(name)); + } + + /// + /// Simulates a click and release at the center of some UI Constrol. + /// + protected async Task ClickControl(Control control) + { + var screenCoords = new ScreenCoordinates( + control.GlobalPixelPosition + control.PixelSize/2, + control.Window?.Id ?? default); + + var relativePos = screenCoords.Position / control.UIScale - control.GlobalPosition; + var relativePixelPos = screenCoords.Position - control.GlobalPixelPosition; + + var args = new GUIBoundKeyEventArgs( + EngineKeyFunctions.UIClick, + BoundKeyState.Down, + screenCoords, + default, + relativePos, + relativePixelPos); + + await Client.DoGuiEvent(control, args); + await RunTicks(1); + + args = new GUIBoundKeyEventArgs( + EngineKeyFunctions.UIClick, + BoundKeyState.Up, + screenCoords, + default, + relativePos, + relativePixelPos); + + await Client.DoGuiEvent(control, args); + await RunTicks(1); + } + + /// + /// Attempts to find a control on some client-side window. Will fail if the control cannot be found. + /// + protected TControl GetControl(string name) + where TWindow : BaseWindow + where TControl : Control + { + var control = GetControl(name); + Assert.That(control.GetType().IsAssignableTo(typeof(TControl))); + return (TControl) control; + } + + protected Control GetControl(string name) where TWindow : BaseWindow + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + var field = typeof(TWindow).GetField(name, flags); + var prop = typeof(TWindow).GetProperty(name, flags); + + if (field == null && prop == null) + { + Assert.Fail($"Window {typeof(TWindow).Name} does not have a field or property named {name}"); + return default!; + } + + var window = GetWindow(); + var control = (field?.GetValue(window) ?? prop?.GetValue(window)) as Control; + + if (control == null) + { + Assert.Fail($"{name} was null or was not a control."); + return default!; + } + + return control; + } + + /// + /// Attempts to find a currently open client-side window. Will fail if the window cannot be found. + /// + /// + /// Note that this just returns the very first open window of this type that is found. + /// + protected TWindow GetWindow() where TWindow : BaseWindow + { + if (TryFindWindow(out TWindow? window)) + return window; + + Assert.Fail($"Could not find a window assignable to {nameof(TWindow)}"); + return default!; + } + + /// + /// Attempts to find a currently open client-side window. + /// + /// + /// Note that this just returns the very first open window of this type that is found. + /// + protected bool TryFindWindow([NotNullWhen(true)] out TWindow? window) where TWindow : BaseWindow + { + TryFindWindow(typeof(TWindow), out var control); + window = control as TWindow; + return window != null; + } + + + /// + /// Attempts to find a currently open client-side window. + /// + /// + /// Note that this just returns the very first open window of this type that is found. + /// + protected bool TryFindWindow(Type type, [NotNullWhen(true)] out BaseWindow? window) + { + Assert.That(type.IsAssignableTo(typeof(BaseWindow))); + window = UiMan.WindowRoot.Children + .OfType() + .Where(x => x.IsOpen) + .FirstOrDefault(x => x.GetType().IsAssignableTo(type)); + + return window != null; + } + + #endregion + #region Power protected void ToggleNeedPower(EntityUid? target = null) diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 16f6c71ca0..6c2c2296e7 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -14,6 +14,7 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using NUnit.Framework; using Robust.Client.GameObjects; +using Robust.Client.UserInterface; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -91,6 +92,7 @@ public abstract partial class InteractionTest // CLIENT dependencies protected IEntityManager CEntMan = default!; + protected IUserInterfaceManager UiMan = default!; protected ConstructionSystem CConSys = default!; protected ExamineSystem ExamineSys = default!; protected InteractionTestSystem CTestSystem = default!; @@ -98,7 +100,6 @@ public abstract partial class InteractionTest // player components protected HandsComponent Hands = default!; protected DoAfterComponent DoAfters = default!; - protected UserInterfaceSystem CUISystem = default!; public float TickPeriod => (float)Timing.TickPeriod.TotalSeconds; @@ -125,10 +126,10 @@ public abstract partial class InteractionTest // client dependencies CEntMan = Client.ResolveDependency(); + UiMan = Client.ResolveDependency(); CTestSystem = CEntMan.System(); CConSys = CEntMan.System(); ExamineSys = CEntMan.System(); - CUISystem = CEntMan.System(); // Setup map. MapData = await PoolManager.CreateTestMap(PairTracker); -- 2.51.2