diff --git a/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs b/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs new file mode 100644 index 0000000000..cbe3ee4dce --- /dev/null +++ b/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Chemistry; +using Content.Shared.Containers.ItemSlots; +using NUnit.Framework; + +namespace Content.IntegrationTests.Tests.Chemistry; + +public sealed class DispenserTest : InteractionTest +{ + /// + /// Basic test that checks that a beaker can be inserted and ejected from a dispenser. + /// + [Test] + public async Task InsertEjectBuiTest() + { + await SpawnTarget("chem_dispenser"); + ToggleNeedPower(); + + // Insert beaker + await Interact("Beaker"); + Assert.IsNull(Hands.ActiveHandEntity); + + // Open BUI + await Interact(""); + + // Eject beaker via BUI. + var ev = new ItemSlotButtonPressedEvent(SharedChemMaster.InputSlotName); + await SendBui(ReagentDispenserUiKey.Key, ev); + + // Beaker is back in the player's hands + Assert.IsNotNull(Hands.ActiveHandEntity); + AssertPrototype("Beaker", Hands.ActiveHandEntity); + } +} diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs index 3949389919..2e8314ec97 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs @@ -62,6 +62,10 @@ public abstract partial class InteractionTest return; Converted = true; + + if (string.IsNullOrWhiteSpace(Prototype)) + return; + if (protoMan.HasIndex(Prototype)) return; @@ -121,6 +125,6 @@ public abstract partial class InteractionTest var meta = SEntMan.GetComponent(uid); Assert.NotNull(meta.EntityPrototype); - return new (meta.EntityPrototype.ID, 1) { Converted = true }; + return new (meta.EntityPrototype!.ID, 1) { Converted = true }; } } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index d07af1dd52..634755062b 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -1,14 +1,17 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Content.Client.Construction; using Content.Server.Construction.Components; +using Content.Server.Power.Components; using Content.Server.Tools.Components; using Content.Shared.Construction.Prototypes; using Content.Shared.Item; using NUnit.Framework; +using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Log; using Robust.Shared.Map; @@ -139,7 +142,7 @@ public abstract partial class InteractionTest await DeleteHeldEntity(); - if (entity == null) + if (entity == null || string.IsNullOrWhiteSpace(entity.Prototype)) { await RunTicks(1); Assert.That(Hands.ActiveHandEntity == null); @@ -238,13 +241,19 @@ public abstract partial class InteractionTest /// /// Place an entity prototype into the players hand and interact with the given entity (or target position) /// - protected async Task Interact(string? id, int quantity = 1, bool shouldSucceed = true, bool awaitDoAfters = true) - => await Interact(id == null ? null : (id, quantity), shouldSucceed, awaitDoAfters); + /// + /// Empty strings imply empty hands. + /// + protected async Task Interact(string id, int quantity = 1, bool shouldSucceed = true, bool awaitDoAfters = true) + => await Interact((id, quantity), shouldSucceed, awaitDoAfters); /// /// Place an entity prototype into the players hand and interact with the given entity (or target position) /// - protected async Task Interact(EntitySpecifier? entity, bool shouldSucceed = true, bool awaitDoAfters = true) + /// + /// Empty strings imply empty hands. + /// + protected async Task Interact(EntitySpecifier entity, bool shouldSucceed = true, bool awaitDoAfters = true) { // For every interaction, we will also examine the entity, just in case this breaks something, somehow. // (e.g., servers attempt to assemble construction examine hints). @@ -376,7 +385,10 @@ public abstract partial class InteractionTest /// /// Variant of that performs several interactions using different entities. /// - protected async Task Interact(params EntitySpecifier?[] specifiers) + /// + /// Empty strings imply empty hands. + /// + protected async Task Interact(params EntitySpecifier[] specifiers) { foreach (var spec in specifiers) { @@ -386,32 +398,60 @@ public abstract partial class InteractionTest #region Asserts - protected void AssertPrototype(string? prototype) + protected void AssertPrototype(string? prototype, EntityUid? target = null) { - var meta = Comp(); + target ??= Target; + if (target == null) + { + Assert.Fail("No target specified"); + return; + } + + var meta = SEntMan.GetComponent(target.Value); Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype)); } - protected void AssertAnchored(bool anchored = true) + protected void AssertAnchored(bool anchored = true, EntityUid? target = null) { - var sXform = SEntMan.GetComponent(Target!.Value); - var cXform = CEntMan.GetComponent(Target.Value); + target ??= Target; + if (target == null) + { + Assert.Fail("No target specified"); + return; + } + + var sXform = SEntMan.GetComponent(target.Value); + var cXform = CEntMan.GetComponent(target.Value); Assert.That(sXform.Anchored, Is.EqualTo(anchored)); Assert.That(cXform.Anchored, Is.EqualTo(anchored)); } - protected void AssertDeleted(bool deleted = true) + protected void AssertDeleted(bool deleted = true, EntityUid? target = null) { - Assert.That(SEntMan.Deleted(Target), Is.EqualTo(deleted)); - Assert.That(CEntMan.Deleted(Target), Is.EqualTo(deleted)); + target ??= Target; + if (target == null) + { + Assert.Fail("No target specified"); + return; + } + + Assert.That(SEntMan.Deleted(target), Is.EqualTo(deleted)); + Assert.That(CEntMan.Deleted(target), Is.EqualTo(deleted)); } /// /// Assert whether or not the target has the given component. /// - protected void AssertComp(bool hasComp = true) + protected void AssertComp(bool hasComp = true, EntityUid? target = null) { - Assert.That(SEntMan.HasComponent(Target), Is.EqualTo(hasComp)); + target ??= Target; + if (target == null) + { + Assert.Fail("No target specified"); + return; + } + + Assert.That(SEntMan.HasComponent(target), Is.EqualTo(hasComp)); } /// @@ -553,7 +593,6 @@ public abstract partial class InteractionTest #endregion - /// /// List of currently active DoAfters on the player. /// @@ -563,7 +602,14 @@ public abstract partial class InteractionTest /// /// Convenience method to get components on the target. Returns SERVER-SIDE components. /// - protected T Comp() => SEntMan.GetComponent(Target!.Value); + protected T Comp(EntityUid? target = null) + { + target ??= Target; + if (target == null) + Assert.Fail("No target specified"); + + return SEntMan.GetComponent(target!.Value); + } /// /// Set the tile at the target position to some prototype. @@ -611,4 +657,77 @@ public abstract partial class InteractionTest protected async Task RunSeconds(float seconds) => await RunTicks((int) Math.Ceiling(seconds / TickPeriod)); + + #region BUI + /// + /// Sends a bui message using the given bui key. + /// + protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, EntityUid? target = null) + { + if (!TryGetBui(key, out var bui)) + return; + + await Client.WaitPost(() => bui.SendMessage(msg)); + + // allow for client -> server and server -> client messages to be sent. + await RunTicks(15); + } + + /// + /// Sends a bui message using the given bui key. + /// + protected async Task CloseBui(Enum key, EntityUid? target = null) + { + if (!TryGetBui(key, out var bui)) + return; + + await Client.WaitPost(() => bui.Close()); + + // allow for client -> server and server -> client messages to be sent. + await RunTicks(15); + } + + protected bool TryGetBui(Enum key, [NotNullWhen(true)] out BoundUserInterface? bui, EntityUid? target = null, bool shouldSucceed = true) + { + bui = null; + target ??= Target; + if (target == null) + { + Assert.Fail("No target specified"); + return false; + } + + if (!CEntMan.TryGetComponent(target, out ClientUserInterfaceComponent? ui)) + { + if (shouldSucceed) + Assert.Fail($"Entity {SEntMan.ToPrettyString(target.Value)} does not have a bui component"); + return false; + } + + var first = ui.Interfaces.First(); + + + bui = ui.Interfaces.FirstOrDefault(x => x.UiKey.Equals(key)); + if (bui == null) + { + if (shouldSucceed) + Assert.Fail($"Entity {SEntMan.ToPrettyString(target.Value)} does not have an open bui with key {key.GetType()}.{key}."); + return false; + } + + Assert.That(shouldSucceed, Is.True); + return true; + } + + #endregion + + #region Power + + protected void ToggleNeedPower(EntityUid? target = null) + { + var comp = Comp(target); + comp.NeedsPower = !comp.NeedsPower; + } + + #endregion } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 582e68aac7..16f6c71ca0 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -13,6 +13,7 @@ using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using NUnit.Framework; +using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -97,11 +98,12 @@ 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; [SetUp] - public async Task Setup() + public virtual async Task Setup() { PairTracker = await PoolManager.GetServerClient(new PoolSettings()); @@ -126,6 +128,7 @@ public abstract partial class InteractionTest CTestSystem = CEntMan.System(); CConSys = CEntMan.System(); ExamineSys = CEntMan.System(); + CUISystem = CEntMan.System(); // Setup map. MapData = await PoolManager.CreateTestMap(PairTracker); @@ -189,7 +192,7 @@ public abstract partial class InteractionTest } [TearDown] - public async Task Cleanup() + public virtual async Task Cleanup() { await Server.WaitPost(() => MapMan.DeleteMap(MapId)); await PairTracker.CleanReturnAsync();