]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add StorageInteractionTest (#28541)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Mon, 3 Jun 2024 21:05:51 +0000 (09:05 +1200)
committerGitHub <noreply@github.com>
Mon, 3 Jun 2024 21:05:51 +0000 (17:05 -0400)
18 files changed:
Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs
Content.IntegrationTests/Tests/Construction/Interaction/ComputerContruction.cs
Content.IntegrationTests/Tests/Construction/Interaction/GrilleWindowConstruction.cs
Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
Content.IntegrationTests/Tests/Construction/Interaction/PanelScrewing.cs
Content.IntegrationTests/Tests/Construction/Interaction/PlaceableDeconstruction.cs
Content.IntegrationTests/Tests/Construction/Interaction/WallConstruction.cs
Content.IntegrationTests/Tests/Construction/Interaction/WindowConstruction.cs
Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs
Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
Content.IntegrationTests/Tests/Payload/ModularGrenadeTests.cs
Content.IntegrationTests/Tests/Storage/StorageInteractionTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
Content.IntegrationTests/Tests/Weldable/WeldableTests.cs

index a5449308be4a433815b06ac54dcaeb368f0c6cd4..52b7e555a9d35720c08ab5a7e064b8e16b7bd4ef 100644 (file)
@@ -18,7 +18,7 @@ public sealed class DispenserTest : InteractionTest
         ToggleNeedPower();
 
         // Insert beaker
-        await Interact("Beaker");
+        await InteractUsing("Beaker");
         Assert.That(Hands.ActiveHandEntity, Is.Null);
 
         // Open BUI
index 5412469ac5d6b589205640c60e8e265a236ee2d9..8af5edaf31652987f04b09f7da35bf24e0e59dd1 100644 (file)
@@ -16,10 +16,8 @@ public sealed class ComputerConstruction : InteractionTest
         await StartConstruction(Computer);
 
         // Initial interaction (ghost turns into real entity)
-        await Interact(Steel, 5);
-        ClientAssertPrototype(ComputerFrame, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
-        ClientTarget = null;
+        await InteractUsing(Steel, 5);
+        ClientAssertPrototype(ComputerFrame, Target);
 
         // Perform construction steps
         await Interact(
@@ -41,7 +39,7 @@ public sealed class ComputerConstruction : InteractionTest
         await StartDeconstruction(ComputerId);
 
         // Initial interaction turns id computer into generic computer
-        await Interact(Screw);
+        await InteractUsing(Screw);
         AssertPrototype(ComputerFrame);
 
         // Perform deconstruction steps
@@ -71,7 +69,7 @@ public sealed class ComputerConstruction : InteractionTest
         await SpawnTarget(ComputerId);
 
         // Initial interaction turns id computer into generic computer
-        await Interact(Screw);
+        await InteractUsing(Screw);
         AssertPrototype(ComputerFrame);
 
         // Perform partial deconstruction steps
index 0de39d27577f22b4d9e4bee5fe4867445702dfa9..ef6a7b09ae34de2fd150b1575ba547e6e75d3018 100644 (file)
@@ -17,17 +17,14 @@ public sealed class GrilleWindowConstruction : InteractionTest
     {
         // Construct Grille
         await StartConstruction(Grille);
-        await Interact(Rod, 10);
-        ClientAssertPrototype(Grille, ClientTarget);
-
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
+        await InteractUsing(Rod, 10);
+        ClientAssertPrototype(Grille, Target);
         var grille = Target;
 
         // Construct Window
         await StartConstruction(Window);
-        await Interact(Glass, 10);
-        ClientAssertPrototype(Window, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
+        await InteractUsing(Glass, 10);
+        ClientAssertPrototype(Window, Target);
 
         // Deconstruct Window
         await Interact(Screw, Wrench);
@@ -35,7 +32,7 @@ public sealed class GrilleWindowConstruction : InteractionTest
 
         // Deconstruct Grille
         Target = grille;
-        await Interact(Cut);
+        await InteractUsing(Cut);
         AssertDeleted();
     }
 
index f52f820a4ce98b207ebd9033215b2ffe5c939c13..06874f39edba2d4dd18fc5e4c6a90e73e6414957 100644 (file)
@@ -14,9 +14,8 @@ public sealed class MachineConstruction : InteractionTest
     public async Task ConstructProtolathe()
     {
         await StartConstruction(MachineFrame);
-        await Interact(Steel, 5);
-        ClientAssertPrototype(Unfinished, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
+        await InteractUsing(Steel, 5);
+        ClientAssertPrototype(Unfinished, Target);
         await Interact(Wrench, Cable);
         AssertPrototype(MachineFrame);
         await Interact(ProtolatheBoard, Bin1, Bin1, Manipulator1, Manipulator1, Beaker, Beaker, Screw);
@@ -51,7 +50,7 @@ public sealed class MachineConstruction : InteractionTest
         AssertPrototype(MachineFrame);
 
         // Change it into an autolathe
-        await Interact("AutolatheMachineCircuitboard");
+        await InteractUsing("AutolatheMachineCircuitboard");
         AssertPrototype(MachineFrame);
         await Interact(Bin1, Bin1, Bin1, Manipulator1, Glass, Screw);
         AssertPrototype("Autolathe");
index b6d960e28821af432b168309946e489e30b85983..636d58bf96fbb7b31a70ef00fe3fc82582cfb165 100644 (file)
@@ -19,21 +19,21 @@ public sealed class PanelScrewing : InteractionTest
 
         // Open & close panel
         Assert.That(comp.Open, Is.False);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.True);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.False);
 
         // Interrupted DoAfters
-        await Interact(Screw, awaitDoAfters: false);
+        await InteractUsing(Screw, awaitDoAfters: false);
         await CancelDoAfters();
         Assert.That(comp.Open, Is.False);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.True);
-        await Interact(Screw, awaitDoAfters: false);
+        await InteractUsing(Screw, awaitDoAfters: false);
         await CancelDoAfters();
         Assert.That(comp.Open, Is.True);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.False);
     }
 }
index bc0cb9bcef38c333af7830d49a5aec10ca288ecb..783c14c0682573177457b1ab187c0d68f704fcd7 100644 (file)
@@ -13,9 +13,9 @@ public sealed class PlaceableDeconstruction : InteractionTest
     {
         await StartDeconstruction("Table");
         Assert.That(Comp<PlaceableSurfaceComponent>().IsPlaceable);
-        await Interact(Wrench);
+        await InteractUsing(Wrench);
         AssertPrototype("TableFrame");
-        await Interact(Wrench);
+        await InteractUsing(Wrench);
         AssertDeleted();
         await AssertEntityLookup((Steel, 1), (Rod, 2));
     }
index 67a2f8025dc087041ea8040944df68fc366ac8e2..292bf0c55abea580308d14feb38b16caa010cc8a 100644 (file)
@@ -12,11 +12,10 @@ public sealed class WallConstruction : InteractionTest
     public async Task ConstructWall()
     {
         await StartConstruction(Wall);
-        await Interact(Steel, 2);
+        await InteractUsing(Steel, 2);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
-        ClientAssertPrototype(Girder, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
-        await Interact(Steel, 2);
+        ClientAssertPrototype(Girder, Target);
+        await InteractUsing(Steel, 2);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
         AssertPrototype(WallSolid);
     }
@@ -25,7 +24,7 @@ public sealed class WallConstruction : InteractionTest
     public async Task DeconstructWall()
     {
         await StartDeconstruction(WallSolid);
-        await Interact(Weld);
+        await InteractUsing(Weld);
         AssertPrototype(Girder);
         await Interact(Wrench, Screw);
         AssertDeleted();
index 46bb892ed99acce4bc44f70fb11efb12345d9f95..2ece6b3e397d2dd86a20ad0f99ed3877bcf349b5 100644 (file)
@@ -11,8 +11,8 @@ public sealed class WindowConstruction : InteractionTest
     public async Task ConstructWindow()
     {
         await StartConstruction(Window);
-        await Interact(Glass, 5);
-        ClientAssertPrototype(Window, ClientTarget);
+        await InteractUsing(Glass, 5);
+        ClientAssertPrototype(Window, Target);
     }
 
     [Test]
@@ -28,8 +28,8 @@ public sealed class WindowConstruction : InteractionTest
     public async Task ConstructReinforcedWindow()
     {
         await StartConstruction(RWindow);
-        await Interact(RGlass, 5);
-        ClientAssertPrototype(RWindow, ClientTarget);
+        await InteractUsing(RGlass, 5);
+        ClientAssertPrototype(RWindow, Target);
     }
 
     [Test]
index abd4bc265b30592156091d6888cb5c626f5bc479..6eea519af3b50729654d8df0252022bbaa668761 100644 (file)
@@ -24,7 +24,7 @@ public sealed class WindowRepair : InteractionTest
         Assert.That(comp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero));
 
         // Repair the entity
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero));
 
         // Validate that we can still deconstruct the entity (i.e., that welding deconstruction is not blocked).
index 0ebd17d8879b4bf285f4732dea2e58ea9cfc6335..1aaf4a5184cee217570566e709f3529e0994e104 100644 (file)
@@ -16,31 +16,31 @@ public sealed class DoAfterCancellationTests : InteractionTest
     public async Task CancelWallDeconstruct()
     {
         await StartDeconstruction(WallConstruction.WallSolid);
-        await Interact(Weld, awaitDoAfters: false);
+        await InteractUsing(Weld, awaitDoAfters: false);
 
         // Failed do-after has no effect
         await CancelDoAfters();
         AssertPrototype(WallConstruction.WallSolid);
 
         // Second attempt works fine
-        await Interact(Weld);
+        await InteractUsing(Weld);
         AssertPrototype(WallConstruction.Girder);
 
         // Repeat for wrenching interaction
         AssertAnchored();
-        await Interact(Wrench, awaitDoAfters: false);
+        await InteractUsing(Wrench, awaitDoAfters: false);
         await CancelDoAfters();
         AssertAnchored();
         AssertPrototype(WallConstruction.Girder);
-        await Interact(Wrench);
+        await InteractUsing(Wrench);
         AssertAnchored(false);
 
         // Repeat for screwdriver interaction.
         AssertExists();
-        await Interact(Screw, awaitDoAfters: false);
+        await InteractUsing(Screw, awaitDoAfters: false);
         await CancelDoAfters();
         AssertExists();
-        await Interact(Screw);
+        await InteractUsing(Screw);
         AssertDeleted();
     }
 
@@ -48,17 +48,16 @@ public sealed class DoAfterCancellationTests : InteractionTest
     public async Task CancelWallConstruct()
     {
         await StartConstruction(WallConstruction.Wall);
-        await Interact(Steel, 5, awaitDoAfters: false);
+        await InteractUsing(Steel, 5, awaitDoAfters: false);
         await CancelDoAfters();
 
-        await Interact(Steel, 5);
-        ClientAssertPrototype(WallConstruction.Girder, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
-        await Interact(Steel, 5, awaitDoAfters: false);
+        await InteractUsing(Steel, 5);
+        ClientAssertPrototype(WallConstruction.Girder, Target);
+        await InteractUsing(Steel, 5, awaitDoAfters: false);
         await CancelDoAfters();
         AssertPrototype(WallConstruction.Girder);
 
-        await Interact(Steel, 5);
+        await InteractUsing(Steel, 5);
         AssertPrototype(WallConstruction.WallSolid);
     }
 
@@ -66,11 +65,11 @@ public sealed class DoAfterCancellationTests : InteractionTest
     public async Task CancelTilePry()
     {
         await SetTile(Floor);
-        await Interact(Pry, awaitDoAfters: false);
+        await InteractUsing(Pry, awaitDoAfters: false);
         await CancelDoAfters();
         await AssertTile(Floor);
 
-        await Interact(Pry);
+        await InteractUsing(Pry);
         await AssertTile(Plating);
     }
 
@@ -78,7 +77,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
     public async Task CancelRepeatedTilePry()
     {
         await SetTile(Floor);
-        await Interact(Pry, awaitDoAfters: false);
+        await InteractUsing(Pry, awaitDoAfters: false);
         await RunTicks(1);
         Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
         await AssertTile(Floor);
@@ -89,7 +88,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
         await AssertTile(Floor);
 
         // Third do after will work fine
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
         await AssertTile(Plating);
     }
@@ -102,7 +101,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
 
         Assert.That(comp.IsWelded, Is.False);
 
-        await Interact(Weld, awaitDoAfters: false);
+        await InteractUsing(Weld, awaitDoAfters: false);
         await RunTicks(1);
         Assert.Multiple(() =>
         {
@@ -120,7 +119,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
         });
 
         // Third do after will work fine
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.Multiple(() =>
         {
             Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
@@ -128,7 +127,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
         });
 
         // Repeat test for un-welding
-        await Interact(Weld, awaitDoAfters: false);
+        await InteractUsing(Weld, awaitDoAfters: false);
         await RunTicks(1);
         Assert.Multiple(() =>
         {
@@ -141,7 +140,7 @@ public sealed class DoAfterCancellationTests : InteractionTest
             Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
             Assert.That(comp.IsWelded, Is.True);
         });
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.Multiple(() =>
         {
             Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
index 9e3dbd8863efa9e5a52d192c355fd58e01c8e11b..f5e8c22242eac4b97fa962623198a27318c514df 100644 (file)
@@ -22,7 +22,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
         });
 
         // Remove the key
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -34,7 +34,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
         await AssertEntityLookup(("EncryptionKeyCommon", 1));
 
         // Re-insert a key.
-        await Interact("EncryptionKeyCentCom");
+        await InteractUsing("EncryptionKeyCentCom");
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1));
@@ -59,7 +59,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
         });
 
         // cannot remove keys without opening panel
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.GreaterThan(0));
@@ -68,7 +68,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
         });
 
         // Open panel
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.Multiple(() =>
         {
             Assert.That(panel.Open, Is.True);
@@ -79,7 +79,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
         });
 
         // Now remove the keys
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -87,7 +87,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
         });
 
         // Reinsert a key
-        await Interact("EncryptionKeyCentCom");
+        await InteractUsing("EncryptionKeyCentCom");
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1));
@@ -97,7 +97,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
         });
 
         // Remove it again
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -106,7 +106,7 @@ public sealed class RemoveEncryptionKeys : InteractionTest
 
         // Prying again will start deconstructing the machine.
         AssertPrototype("TelecomServerFilled");
-        await Interact(Pry);
+        await InteractUsing(Pry);
         AssertPrototype("MachineFrame");
     }
 }
index 37dca721373b97248474c6c819959546c26b37e6..053152dbe1bec6727a892006e7a05c7e1d8f8c32 100644 (file)
@@ -33,7 +33,7 @@ public abstract partial class InteractionTest
         public int Quantity;
 
         /// <summary>
-        /// If true, a check has been performed to see if the prototype ia an entity prototype with a stack component,
+        /// If true, a check has been performed to see if the prototype is an entity prototype with a stack component,
         /// in which case the specifier was converted into a stack-specifier
         /// </summary>
         public bool Converted;
@@ -100,7 +100,7 @@ public abstract partial class InteractionTest
 
         if (!ProtoMan.TryIndex<EntityPrototype>(spec.Prototype, out var entProto))
         {
-            Assert.Fail($"Unkown prototype: {spec.Prototype}");
+            Assert.Fail($"Unknown prototype: {spec.Prototype}");
             return default;
         }
 
@@ -120,7 +120,7 @@ public abstract partial class InteractionTest
 
     /// <summary>
     /// Convert an entity-uid to a matching entity specifier. Useful when doing entity lookups & checking that the
-    /// right quantity of entities/materials werre produced. Returns null if passed an entity with a null prototype.
+    /// right quantity of entities/materials were produced. Returns null if passed an entity with a null prototype.
     /// </summary>
     protected EntitySpecifier? ToEntitySpecifier(EntityUid uid)
     {
index 19ca83a9715d5e44d95a682156a9035846de3de7..f4826cb2495c4cf8657ef27198c56cb350472a8f 100644 (file)
@@ -14,6 +14,7 @@ using Content.Shared.Construction.Prototypes;
 using Content.Shared.Gravity;
 using Content.Shared.Item;
 using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.CustomControls;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Input;
@@ -44,8 +45,9 @@ public abstract partial class InteractionTest
                 return;
 
             var comp = CEntMan.GetComponent<ConstructionGhostComponent>(clientTarget!.Value);
-            ClientTarget = clientTarget;
-            ConstructionGhostId = comp.Owner.Id;
+            Target = CEntMan.GetNetEntity(clientTarget.Value);
+            Assert.That(Target.Value.IsClientSide());
+            ConstructionGhostId = clientTarget.Value.GetHashCode();
         });
 
         await RunTicks(1);
@@ -129,21 +131,20 @@ public abstract partial class InteractionTest
     /// <summary>
     /// Place an entity prototype into the players hand. Deletes any currently held entity.
     /// </summary>
-    /// <remarks>
-    /// Automatically enables welders.
-    /// </remarks>
-    protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableWelder = true)
+    /// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
+    /// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
+    /// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
+    protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableToggleable = true)
     {
-        return await PlaceInHands((id, quantity), enableWelder);
+        return await PlaceInHands((id, quantity), enableToggleable);
     }
 
     /// <summary>
     /// Place an entity prototype into the players hand. Deletes any currently held entity.
     /// </summary>
-    /// <remarks>
-    /// Automatically enables welders.
-    /// </remarks>
-    protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableWelder = true)
+    /// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
+    /// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
+    protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
     {
         if (Hands.ActiveHand == null)
         {
@@ -165,7 +166,7 @@ public abstract partial class InteractionTest
             Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
 
             // turn on welders
-            if (enableWelder && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
+            if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
             {
                 Assert.That(ItemToggleSys.TryActivate(item, playerEnt, itemToggle: itemToggle));
             }
@@ -173,7 +174,7 @@ public abstract partial class InteractionTest
 
         await RunTicks(1);
         Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
-        if (enableWelder && itemToggle != null)
+        if (enableToggleable && itemToggle != null)
             Assert.That(itemToggle.Activated);
 
         return SEntMan.GetNetEntity(item);
@@ -254,21 +255,20 @@ public abstract partial class InteractionTest
     /// <summary>
     /// Place an entity prototype into the players hand and interact with the given entity (or target position)
     /// </summary>
-    /// <remarks>
-    /// Empty strings imply empty hands.
-    /// </remarks>
-    protected async Task Interact(string id, int quantity = 1, bool shouldSucceed = true, bool awaitDoAfters = true)
+    /// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
+    /// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
+    /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
+    protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
     {
-        await Interact((id, quantity), shouldSucceed, awaitDoAfters);
+        await InteractUsing((id, quantity), awaitDoAfters);
     }
 
     /// <summary>
-    /// Place an entity prototype into the players hand and interact with the given entity (or target position)
+    /// Place an entity prototype into the players hand and interact with the given entity (or target position).
     /// </summary>
-    /// <remarks>
-    /// Empty strings imply empty hands.
-    /// </remarks>
-    protected async Task Interact(EntitySpecifier entity, bool shouldSucceed = true, bool awaitDoAfters = true)
+    /// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
+    /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
+    protected async Task InteractUsing(EntitySpecifier entity, 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).
@@ -278,38 +278,80 @@ public abstract partial class InteractionTest
         }
 
         await PlaceInHands(entity);
-        await Interact(shouldSucceed, awaitDoAfters);
+        await Interact(awaitDoAfters);
     }
 
     /// <summary>
     /// Interact with an entity using the currently held entity.
     /// </summary>
-    protected async Task Interact(bool shouldSucceed = true, bool awaitDoAfters = true)
+    /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
+    protected async Task Interact(bool awaitDoAfters = true)
     {
-        var clientTarget = ClientTarget;
-
-        if ((clientTarget?.IsValid() != true || CEntMan.Deleted(clientTarget)) && (Target == null || Target.Value.IsValid()))
+        if (Target == null || !Target.Value.IsClientSide())
         {
-            await Server.WaitPost(() => InteractSys.UserInteraction(SEntMan.GetEntity(Player), SEntMan.GetCoordinates(TargetCoords), SEntMan.GetEntity(Target)));
-            await RunTicks(1);
+            await Interact(Target, TargetCoords, awaitDoAfters);
+            return;
         }
-        else
-        {
-            // The entity is client-side, so attempt to start construction
-            var clientEnt = ClientTarget ?? CEntMan.GetEntity(Target);
 
-            await Client.WaitPost(() => CConSys.TryStartConstruction(clientEnt!.Value));
-            await RunTicks(5);
-        }
+        // The target is a client-side entity, so we will just attempt to start construction under the assumption that
+        // it is a construction ghost.
+
+        await Client.WaitPost(() => CConSys.TryStartConstruction(CTarget!.Value));
+        await RunTicks(5);
+
+        if (awaitDoAfters)
+            await AwaitDoAfters();
+
+        await CheckTargetChange();
+    }
+
+    /// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool)"/>
+    protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
+    {
+        Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
+        var coords = SEntMan.GetCoordinates(coordinates);
+        Assert.That(coords.IsValid(SEntMan));
+        await Interact(sTarget, coords, awaitDoAfters);
+    }
+
+    /// <summary>
+    /// Interact with an entity using the currently held entity.
+    /// </summary>
+    protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
+    {
+        Assert.That(SEntMan.TryGetEntity(Player, out var player));
+
+        await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
+        await RunTicks(1);
+
+        if (awaitDoAfters)
+            await AwaitDoAfters();
+
+        await CheckTargetChange();
+    }
+
+    /// <summary>
+    /// Activate an entity.
+    /// </summary>
+    protected async Task Activate(NetEntity? target = null, bool awaitDoAfters = true)
+    {
+        target ??= Target;
+        Assert.That(target, Is.Not.Null);
+        Assert.That(SEntMan.TryGetEntity(target!.Value, out var sTarget));
+        Assert.That(SEntMan.TryGetEntity(Player, out var player));
+
+        await Server.WaitPost(() => InteractSys.InteractionActivate(player!.Value, sTarget!.Value));
+        await RunTicks(1);
 
         if (awaitDoAfters)
-            await AwaitDoAfters(shouldSucceed);
+            await AwaitDoAfters();
 
-        await CheckTargetChange(shouldSucceed && awaitDoAfters);
+        await CheckTargetChange();
     }
 
     /// <summary>
-    /// Variant of <see cref="InteractUsing"/> that performs several interactions using different entities.
+    /// Variant of <see cref="InteractUsing(string,int,bool)"/> that performs several interactions using different entities.
+    /// Useful for quickly finishing multiple construction steps.
     /// </summary>
     /// <remarks>
     /// Empty strings imply empty hands.
@@ -318,7 +360,7 @@ public abstract partial class InteractionTest
     {
         foreach (var spec in specifiers)
         {
-            await Interact(spec);
+            await InteractUsing(spec);
         }
     }
 
@@ -338,7 +380,7 @@ public abstract partial class InteractionTest
     /// <summary>
     /// Wait for any currently active DoAfters to finish.
     /// </summary>
-    protected async Task AwaitDoAfters(bool shouldSucceed = true, int maxExpected = 1)
+    protected async Task AwaitDoAfters(int maxExpected = 1)
     {
         if (!ActiveDoAfters.Any())
             return;
@@ -353,13 +395,12 @@ public abstract partial class InteractionTest
             await RunTicks(10);
         }
 
-        if (!shouldSucceed)
-            return;
-
         foreach (var doAfter in doAfters)
         {
             Assert.That(!doAfter.Cancelled);
         }
+
+        await RunTicks(5);
     }
 
     /// <summary>
@@ -398,39 +439,28 @@ public abstract partial class InteractionTest
     /// Check if the test's target entity has changed. E.g., construction interactions will swap out entities while
     /// a structure is being built.
     /// </summary>
-    protected async Task CheckTargetChange(bool shouldSucceed)
+    protected async Task CheckTargetChange()
     {
         if (Target == null)
             return;
 
-        var target = Target.Value;
+        var originalTarget = Target.Value;
         await RunTicks(5);
 
-        if (ClientTarget != null && CEntMan.IsClientSide(ClientTarget.Value))
+        if (Target.Value.IsClientSide() && CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh))
         {
-            Assert.That(CEntMan.Deleted(ClientTarget.Value), Is.EqualTo(shouldSucceed),
-                $"Construction ghost was {(shouldSucceed ? "not deleted" : "deleted")}.");
-
-            if (shouldSucceed)
-            {
-                Assert.That(CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh),
-                    $"Failed to get construction entity from ghost Id");
-
-                await Client.WaitPost(() => CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}"));
-                Target = newWeh;
-            }
+            CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}");
+            Target = newWeh;
         }
 
         if (STestSystem.EntChanges.TryGetValue(Target.Value, out var newServerWeh))
         {
-            await Server.WaitPost(
-                () => SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}"));
-
+            SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}");
             Target = newServerWeh;
         }
 
-        if (Target != target)
-            await CheckTargetChange(shouldSucceed);
+        if (Target != originalTarget)
+            await CheckTargetChange();
     }
 
     #region Asserts
@@ -444,16 +474,10 @@ public abstract partial class InteractionTest
             return;
         }
 
-        var meta = SEntMan.GetComponent<MetaDataComponent>(SEntMan.GetEntity(target.Value));
+        var meta = CEntMan.GetComponent<MetaDataComponent>(CEntMan.GetEntity(target.Value));
         Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
     }
 
-    protected void ClientAssertPrototype(string? prototype, EntityUid? target)
-    {
-        var netEnt = CTestSystem.Ghosts[target.GetHashCode()];
-        AssertPrototype(prototype, netEnt);
-    }
-
     protected void AssertPrototype(string? prototype, NetEntity? target = null)
     {
         target ??= Target;
@@ -699,6 +723,8 @@ public abstract partial class InteractionTest
     protected IEnumerable<Shared.DoAfter.DoAfter> ActiveDoAfters
         => DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed);
 
+    #region Component
+
     /// <summary>
     /// Convenience method to get components on the target. Returns SERVER-SIDE components.
     /// </summary>
@@ -708,9 +734,23 @@ public abstract partial class InteractionTest
         if (target == null)
             Assert.Fail("No target specified");
 
-        return SEntMan.GetComponent<T>(SEntMan.GetEntity(target!.Value));
+        return SEntMan.GetComponent<T>(ToServer(target!.Value));
+    }
+
+    /// <inheritdoc cref="Comp{T}"/>
+    protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
+    {
+        return SEntMan.TryGetComponent(ToServer(target), out comp);
+    }
+
+    /// <inheritdoc cref="Comp{T}"/>
+    protected bool TryComp<T>([NotNullWhen(true)] out T? comp) where T : IComponent
+    {
+        return SEntMan.TryGetComponent(STarget, out comp);
     }
 
+    #endregion
+
     /// <summary>
     /// Set the tile at the target position to some prototype.
     /// </summary>
@@ -833,23 +873,70 @@ public abstract partial class InteractionTest
         return true;
     }
 
+    protected bool IsUiOpen(Enum key)
+    {
+        if (!TryComp(Player, out UserInterfaceUserComponent? user))
+            return false;
+
+        foreach (var keys in user.OpenInterfaces.Values)
+        {
+            if (keys.Contains(key))
+                return true;
+        }
+
+        return false;
+    }
+
     #endregion
 
     #region UI
 
     /// <summary>
-    ///     Presses and releases a button on some client-side window. Will fail if the button cannot be found.
+    /// Attempts to find, and then presses and releases a control on some client-side window.
+    /// Will fail if the control cannot be found.
+    /// </summary>
+    protected async Task ClickControl<TWindow, TControl>(string name, BoundKeyFunction? function = null)
+        where TWindow : BaseWindow
+        where TControl : Control
+    {
+        var window = GetWindow<TWindow>();
+        var control = GetControlFromField<TControl>(name, window);
+        await ClickControl(control, function);
+    }
+
+    /// <summary>
+    /// Attempts to find, and then presses and releases a control on some client-side widget.
+    /// Will fail if the control cannot be found.
     /// </summary>
-    protected async Task ClickControl<TWindow>(string name) where TWindow : BaseWindow
+    protected async Task ClickWidgetControl<TWidget, TControl>(string name, BoundKeyFunction? function = null)
+        where TWidget : UIWidget, new()
+        where TControl : Control
+    {
+        var widget = GetWidget<TWidget>();
+        var control = GetControlFromField<TControl>(name, widget);
+        await ClickControl(control, function);
+    }
+
+    /// <inheritdoc cref="ClickControl{TWindow,TControl}"/>
+    protected async Task ClickControl<TWindow>(string name, BoundKeyFunction? function = null)
+        where TWindow : BaseWindow
     {
-        await ClickControl(GetControl<TWindow, Control>(name));
+        await ClickControl<TWindow, Control>(name, function);
+    }
+
+    /// <inheritdoc cref="ClickWidgetControl{TWidget,TControl}"/>
+    protected async Task ClickWidgetControl<TWidget>(string name, BoundKeyFunction? function = null)
+        where TWidget : UIWidget, new()
+    {
+        await ClickWidgetControl<TWidget, Control>(name, function);
     }
 
     /// <summary>
-    ///     Simulates a click and release at the center of some UI Constrol.
+    ///     Simulates a click and release at the center of some UI control.
     /// </summary>
-    protected async Task ClickControl(Control control)
+    protected async Task ClickControl(Control control, BoundKeyFunction? function = null)
     {
+        function ??= EngineKeyFunctions.UIClick;
         var screenCoords = new ScreenCoordinates(
             control.GlobalPixelPosition + control.PixelSize / 2,
             control.Window?.Id ?? default);
@@ -858,7 +945,7 @@ public abstract partial class InteractionTest
         var relativePixelPos = screenCoords.Position - control.GlobalPixelPosition;
 
         var args = new GUIBoundKeyEventArgs(
-            EngineKeyFunctions.UIClick,
+            function.Value,
             BoundKeyState.Down,
             screenCoords,
             default,
@@ -869,7 +956,7 @@ public abstract partial class InteractionTest
         await RunTicks(1);
 
         args = new GUIBoundKeyEventArgs(
-            EngineKeyFunctions.UIClick,
+            function.Value,
             BoundKeyState.Up,
             screenCoords,
             default,
@@ -881,31 +968,26 @@ public abstract partial class InteractionTest
     }
 
     /// <summary>
-    ///     Attempts to find a control on some client-side window. Will fail if the control cannot be found.
+    /// Attempt to retrieve a control by looking for a field on some other control.
     /// </summary>
-    protected TControl GetControl<TWindow, TControl>(string name)
-        where TWindow : BaseWindow
+    /// <remarks>
+    /// Will fail if the control cannot be found.
+    /// </remarks>
+    protected TControl GetControlFromField<TControl>(string name, Control parent)
         where TControl : Control
-    {
-        var control = GetControl<TWindow>(name);
-        Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
-        return (TControl) control;
-    }
-
-    protected Control GetControl<TWindow>(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);
+        var parentType = parent.GetType();
+        var field = parentType.GetField(name, flags);
+        var prop = parentType.GetProperty(name, flags);
 
         if (field == null && prop == null)
         {
-            Assert.Fail($"Window {typeof(TWindow).Name} does not have a field or property named {name}");
+            Assert.Fail($"Window {parentType.Name} does not have a field or property named {name}");
             return default!;
         }
 
-        var window = GetWindow<TWindow>();
-        var fieldOrProp = field?.GetValue(window) ?? prop?.GetValue(window);
+        var fieldOrProp = field?.GetValue(parent) ?? prop?.GetValue(parent);
 
         if (fieldOrProp is not Control control)
         {
@@ -913,7 +995,59 @@ public abstract partial class InteractionTest
             return default!;
         }
 
-        return control;
+        Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
+        return (TControl) control;
+    }
+
+    /// <summary>
+    /// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
+    /// </summary>
+    /// <remarks>
+    /// Will fail if the control cannot be found.
+    /// </remarks>
+    protected TControl GetControlFromChildren<TControl>(Func<TControl, bool> predicate, Control parent, bool recursive = true)
+        where TControl : Control
+    {
+        if (TryGetControlFromChildren(predicate, parent, out var control, recursive))
+            return control;
+
+        Assert.Fail($"Failed to find a {nameof(TControl)} that satisfies the predicate in {parent.Name}");
+        return default!;
+    }
+
+    /// <summary>
+    /// Attempt to retrieve a control of a given type by iterating through a control's children.
+    /// </summary>
+    protected TControl GetControlFromChildren<TControl>(Control parent, bool recursive = false)
+        where TControl : Control
+    {
+        return GetControlFromChildren<TControl>(static _ => true, parent, recursive);
+    }
+
+    /// <summary>
+    /// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
+    /// </summary>
+    protected bool TryGetControlFromChildren<TControl>(
+        Func<TControl, bool> predicate,
+        Control parent,
+        [NotNullWhen(true)] out TControl? control,
+        bool recursive = true)
+        where TControl : Control
+    {
+        foreach (var ctrl in parent.Children)
+        {
+            if (ctrl is TControl cast && predicate(cast))
+            {
+                control = cast;
+                return true;
+            }
+
+            if (recursive && TryGetControlFromChildren(predicate, ctrl, out control))
+                return true;
+        }
+
+        control = null;
+        return false;
     }
 
     /// <summary>
@@ -944,7 +1078,6 @@ public abstract partial class InteractionTest
         return window != null;
     }
 
-
     /// <summary>
     /// Attempts to find a currently open client-side window.
     /// </summary>
@@ -962,6 +1095,34 @@ public abstract partial class InteractionTest
         return window != null;
     }
 
+
+    /// <summary>
+    /// Attempts to find client-side UI widget.
+    /// </summary>
+    protected UIWidget GetWidget<TWidget>()
+        where TWidget : UIWidget, new()
+    {
+        if (TryFindWidget(out TWidget? widget))
+            return widget;
+
+        Assert.Fail($"Could not find a {typeof(TWidget).Name} widget");
+        return default!;
+    }
+
+    /// <summary>
+    /// Attempts to find client-side UI widget.
+    /// </summary>
+    private bool TryFindWidget<TWidget>([NotNullWhen(true)] out TWidget? uiWidget)
+        where TWidget : UIWidget, new()
+    {
+        uiWidget = null;
+        var screen = UiMan.ActiveScreen;
+        if (screen == null)
+            return false;
+
+        return screen.TryGetWidget(out uiWidget);
+    }
+
     #endregion
 
     #region Power
index e5f794feaa04e6541bc14d367b4eb5cb34064b52..089addfaefd063106b097f0f73a4db35308056bd 100644 (file)
@@ -1,8 +1,10 @@
 #nullable enable
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Numerics;
 using Content.Client.Construction;
 using Content.Client.Examine;
+using Content.Client.Gameplay;
 using Content.IntegrationTests.Pair;
 using Content.Server.Body.Systems;
 using Content.Server.Hands.Systems;
@@ -24,6 +26,7 @@ using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 using Robust.UnitTesting;
 using Content.Shared.Item.ItemToggle;
+using Robust.Client.State;
 
 namespace Content.IntegrationTests.Tests.Interaction;
 
@@ -64,15 +67,12 @@ public abstract partial class InteractionTest
     /// The player entity that performs all these interactions. Defaults to an admin-observer with 1 hand.
     /// </summary>
     protected NetEntity Player;
-
-    protected EntityUid SPlayer => ToServer(Player);
-    protected EntityUid CPlayer => ToClient(Player);
+    protected EntityUid SPlayer;
+    protected EntityUid CPlayer;
 
     protected ICommonSession ClientSession = default!;
     protected ICommonSession ServerSession = default!;
 
-    public EntityUid? ClientTarget;
-
     /// <summary>
     /// The current target entity. This is the default entity for various helper functions.
     /// </summary>
@@ -108,6 +108,7 @@ public abstract partial class InteractionTest
     protected InteractionTestSystem STestSystem = default!;
     protected SharedTransformSystem Transform = default!;
     protected ISawmill SLogger = default!;
+    protected SharedUserInterfaceSystem SUiSys = default!;
 
     // CLIENT dependencies
     protected IEntityManager CEntMan = default!;
@@ -119,6 +120,7 @@ public abstract partial class InteractionTest
     protected ExamineSystem ExamineSys = default!;
     protected InteractionTestSystem CTestSystem = default!;
     protected ISawmill CLogger = default!;
+    protected SharedUserInterfaceSystem CUiSys = default!;
 
     // player components
     protected HandsComponent Hands = default!;
@@ -168,6 +170,7 @@ public abstract partial class InteractionTest
         STestSystem = SEntMan.System<InteractionTestSystem>();
         Stack = SEntMan.System<StackSystem>();
         SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
+        SUiSys = Client.System<SharedUserInterfaceSystem>();
 
         // client dependencies
         CEntMan = Client.ResolveDependency<IEntityManager>();
@@ -179,6 +182,7 @@ public abstract partial class InteractionTest
         CConSys = CEntMan.System<ConstructionSystem>();
         ExamineSys = CEntMan.System<ExamineSystem>();
         CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
+        CUiSys = Client.System<SharedUserInterfaceSystem>();
 
         // Setup map.
         await Pair.CreateTestMap();
@@ -204,15 +208,16 @@ public abstract partial class InteractionTest
 
             old = cPlayerMan.LocalEntity;
             Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)));
-            var serverPlayerEnt = SEntMan.GetEntity(Player);
-            Server.PlayerMan.SetAttachedEntity(ServerSession, serverPlayerEnt);
-            Hands = SEntMan.GetComponent<HandsComponent>(serverPlayerEnt);
-            DoAfters = SEntMan.GetComponent<DoAfterComponent>(serverPlayerEnt);
+            SPlayer = SEntMan.GetEntity(Player);
+            Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
+            Hands = SEntMan.GetComponent<HandsComponent>(SPlayer);
+            DoAfters = SEntMan.GetComponent<DoAfterComponent>(SPlayer);
         });
 
         // Check player got attached.
         await RunTicks(5);
-        Assert.That(CEntMan.GetNetEntity(cPlayerMan.LocalEntity), Is.EqualTo(Player));
+        CPlayer = ToClient(Player);
+        Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(CPlayer));
 
         // Delete old player entity.
         await Server.WaitPost(() =>
@@ -235,6 +240,10 @@ public abstract partial class InteractionTest
             }
         });
 
+        // Change UI state to in-game.
+        var state = Client.ResolveDependency<IStateManager>();
+        await Client.WaitPost(() => state.RequestStateChange<GameplayState>());
+
         // Final player asserts/checks.
         await Pair.ReallyBeIdle(5);
         Assert.Multiple(() =>
index 70179fdec1aae8c636886a474e87a653440a416b..4db79373d38933495c08352826fbc120800c13ee 100644 (file)
@@ -22,32 +22,32 @@ public sealed class ModularGrenadeTests : InteractionTest
         Target = SEntMan.GetNetEntity(await FindEntity("ModularGrenade"));
 
         await Drop();
-        await Interact(Cable);
+        await InteractUsing(Cable);
 
         // Insert & remove trigger
         AssertComp<OnUseTimerTriggerComponent>(false);
-        await Interact(Trigger);
+        await InteractUsing(Trigger);
         AssertComp<OnUseTimerTriggerComponent>();
         await FindEntity(Trigger, LookupFlags.Uncontained, shouldSucceed: false);
-        await Interact(Pry);
+        await InteractUsing(Pry);
         AssertComp<OnUseTimerTriggerComponent>(false);
 
         // Trigger was dropped to floor, not deleted.
         await FindEntity(Trigger, LookupFlags.Uncontained);
 
         // Re-insert
-        await Interact(Trigger);
+        await InteractUsing(Trigger);
         AssertComp<OnUseTimerTriggerComponent>();
 
         // Insert & remove payload.
-        await Interact(Payload);
+        await InteractUsing(Payload);
         await FindEntity(Payload, LookupFlags.Uncontained, shouldSucceed: false);
-        await Interact(Pry);
+        await InteractUsing(Pry);
         var ent = await FindEntity(Payload, LookupFlags.Uncontained);
         await Delete(ent);
 
         // successfully insert a second time
-        await Interact(Payload);
+        await InteractUsing(Payload);
         ent = await FindEntity(Payload);
         var sys = SEntMan.System<SharedContainerSystem>();
         Assert.That(sys.IsEntityInContainer(ent));
diff --git a/Content.IntegrationTests/Tests/Storage/StorageInteractionTest.cs b/Content.IntegrationTests/Tests/Storage/StorageInteractionTest.cs
new file mode 100644 (file)
index 0000000..34402dd
--- /dev/null
@@ -0,0 +1,75 @@
+using Content.Client.UserInterface.Systems.Hotbar.Widgets;
+using Content.Client.UserInterface.Systems.Storage.Controls;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Input;
+using Content.Shared.PDA;
+using Content.Shared.Storage;
+using Robust.Client.UserInterface;
+using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests.Storage;
+
+public sealed class StorageInteractionTest : InteractionTest
+{
+    /// <summary>
+    /// Check that players can interact with items in storage if the storage UI is open
+    /// </summary>
+    [Test]
+    public async Task UiInteractTest()
+    {
+        var sys = Server.System<SharedContainerSystem>();
+
+        await SpawnTarget("ClothingBackpack");
+        var backpack = ToServer(Target);
+
+        // Initially no BUI is open.
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.False);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
+
+        // Activating the backpack opens the UI
+        await Activate();
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
+
+        // Pick up a PDA
+        var pda = await PlaceInHands("PassengerPDA");
+        var sPda = ToServer(pda);
+        Assert.That(sys.IsEntityInContainer(sPda), Is.True);
+        Assert.That(sys.TryGetContainingContainer((sPda, null), out var container));
+        Assert.That(container!.Owner, Is.EqualTo(SPlayer));
+
+        // Insert the PDA into the backpack
+        await Interact();
+        Assert.That(sys.TryGetContainingContainer((sPda, null), out container));
+        Assert.That(container!.Owner, Is.EqualTo(backpack));
+
+        // Use "e" / ActivateInWorld to open the PDA UI while it is still in the backpack.
+        var ctrl = GetStorageControl(pda);
+        await ClickControl(ctrl, ContentKeyFunctions.ActivateItemInWorld);
+        await RunTicks(10);
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
+
+        // Click on the pda to pick it up and remove it from the backpack.
+        await ClickControl(ctrl, ContentKeyFunctions.MoveStoredItem);
+        await RunTicks(10);
+        Assert.That(sys.TryGetContainingContainer((sPda, null), out container));
+        Assert.That(container!.Owner, Is.EqualTo(SPlayer));
+
+        // UIs should still be open
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
+    }
+
+    /// <summary>
+    /// Retrieve the control that corresponds to the given entity in the currently open storage UI.
+    /// </summary>
+    private ItemGridPiece GetStorageControl(NetEntity target)
+    {
+        var uid = ToClient(target);
+        var hotbar = GetWidget<HotbarGui>();
+        var storageContainer  = GetControlFromField<Control>(nameof(HotbarGui.StorageContainer), hotbar);
+        return GetControlFromChildren<ItemGridPiece>(c => c.Entity == uid, storageContainer);
+    }
+}
index 083e817d69754ea8f755ba5d4f2265bd5f3b179f..6ea8b6882ad3df8545fdcb2b33ae7dfd0f7f5b4b 100644 (file)
@@ -15,10 +15,10 @@ public sealed class TileConstructionTests : InteractionTest
         await AssertTile(Plating, PlayerCoords);
         AssertGridCount(1);
         await SetTile(null);
-        await Interact(Rod);
+        await InteractUsing(Rod);
         await AssertTile(Lattice);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
-        await Interact(Cut);
+        await InteractUsing(Cut);
         await AssertTile(null);
         await AssertEntityLookup((Rod, 1));
         AssertGridCount(1);
@@ -43,14 +43,14 @@ public sealed class TileConstructionTests : InteractionTest
         // Place Lattice
         var oldPos = TargetCoords;
         TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
-        await Interact(Rod);
+        await InteractUsing(Rod);
         TargetCoords = oldPos;
         await AssertTile(Lattice);
         AssertGridCount(1);
 
         // Cut lattice
         Assert.That(Hands.ActiveHandEntity, Is.Null);
-        await Interact(Cut);
+        await InteractUsing(Cut);
         await AssertTile(null);
         AssertGridCount(0);
 
@@ -76,25 +76,25 @@ public sealed class TileConstructionTests : InteractionTest
         // Space -> Lattice
         var oldPos = TargetCoords;
         TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
-        await Interact(Rod);
+        await InteractUsing(Rod);
         TargetCoords = oldPos;
         await AssertTile(Lattice);
         AssertGridCount(1);
 
         // Lattice -> Plating
-        await Interact(Steel);
+        await InteractUsing(Steel);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
         await AssertTile(Plating);
         AssertGridCount(1);
 
         // Plating -> Tile
-        await Interact(FloorItem);
+        await InteractUsing(FloorItem);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
         await AssertTile(Floor);
         AssertGridCount(1);
 
         // Tile -> Plating
-        await Interact(Pry);
+        await InteractUsing(Pry);
         await AssertTile(Plating);
         AssertGridCount(1);
 
index 6227f3dee1b9fc6ad950f658e464c501c9f674ac..e7eadeda0a475cc87697425cb0a7b151c0416d26 100644 (file)
@@ -18,7 +18,7 @@ public sealed class WeldableTests : InteractionTest
 
         Assert.That(comp.IsWelded, Is.False);
 
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.That(comp.IsWelded, Is.True);
         AssertPrototype(Locker); // Prototype did not change.
     }