#include "catch.hpp" #include #include #include "DebrisSelectionChangedEvent.h" #include "EntitySelectionChangedEvent.h" #include "EventHandler.h" #include "EventManager.h" #include "SelectionChangedEvent.h" #include "SelectionController.h" // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- // Counts the change events the controller publishes, so the tests can assert not // just the resulting selection but that the widgets were told about it — and, for // the categories a change did not touch, that they were not told needlessly. class SelectionEventSpy : public CombinedEventHandler { public: SelectionEventSpy() { registerForEvents(); } ~SelectionEventSpy() { unregisterForEvents(); } int buildingEvents = 0; int actorEvents = 0; int debrisEvents = 0; void reset() { buildingEvents = 0; actorEvents = 0; debrisEvents = 0; } private: void handleEvent(std::shared_ptr /*event*/) override { ++buildingEvents; } void handleEvent(std::shared_ptr /*event*/) override { ++actorEvents; } void handleEvent(std::shared_ptr /*event*/) override { ++debrisEvents; } }; static entt::entity makeEntity(int index) { return static_cast(index); } // --------------------------------------------------------------------------- // Category precedence (REQ-UI-SELECTION-CATEGORIES) // --------------------------------------------------------------------------- TEST_CASE("Selecting a building clears actors and debris", "[selection]") { SelectionController controller; controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)}, SelectionMode::Replace); controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace); REQUIRE(controller.getSelectedBuildings() == std::vector{BuildingId(7)}); REQUIRE(controller.getSelectedActors().empty()); REQUIRE(controller.getSelectedDebris().empty()); } TEST_CASE("Selecting a field object clears buildings", "[selection]") { SelectionController controller; controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace); controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Replace); REQUIRE(controller.getSelectedBuildings().empty()); REQUIRE(controller.getSelectedActors() == std::vector{makeEntity(1)}); } TEST_CASE("Actors and debris coexist", "[selection]") { // The one pair of categories that does not evict each other // (REQ-UI-MULTI-SELECT, REQ-UI-DEBRIS-MULTI-SELECT). SelectionController controller; controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)}, SelectionMode::Replace); REQUIRE(controller.getSelectedActors() == std::vector{makeEntity(1)}); REQUIRE(controller.getSelectedDebris() == std::vector{makeEntity(2)}); } TEST_CASE("A plain click on an actor drops any selected debris", "[selection]") { // A point click passes only the category it hit, so Replace with an empty // debris list is what "this actor and nothing else" means // (REQ-UI-ENTITY-CLICK-SELECT). SelectionController controller; controller.selectFieldObjects({}, {makeEntity(2)}, SelectionMode::Replace); controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Replace); REQUIRE(controller.getSelectedActors() == std::vector{makeEntity(1)}); REQUIRE(controller.getSelectedDebris().empty()); } // --------------------------------------------------------------------------- // Replace / Toggle / Add // --------------------------------------------------------------------------- TEST_CASE("Replace makes the hit the whole selection", "[selection]") { SelectionController controller; controller.selectBuildings({BuildingId(1), BuildingId(2)}, SelectionMode::Replace); controller.selectBuildings({BuildingId(3)}, SelectionMode::Replace); REQUIRE(controller.getSelectedBuildings() == std::vector{BuildingId(3)}); } TEST_CASE("Toggle adds a building that was not selected", "[selection]") { SelectionController controller; controller.selectBuildings({BuildingId(1)}, SelectionMode::Replace); controller.selectBuildings({BuildingId(2)}, SelectionMode::Toggle); REQUIRE(controller.getSelectedBuildings() == std::vector{BuildingId(1), BuildingId(2)}); } TEST_CASE("Toggle removes a building that was already selected", "[selection]") { // Ctrl+clicking a selected building deselects it, leaving the rest alone. SelectionController controller; controller.selectBuildings({BuildingId(1), BuildingId(2), BuildingId(3)}, SelectionMode::Replace); controller.selectBuildings({BuildingId(2)}, SelectionMode::Toggle); REQUIRE(controller.getSelectedBuildings() == std::vector{BuildingId(1), BuildingId(3)}); } TEST_CASE("Add never deselects", "[selection]") { // This is where a Ctrl box drag differs from a Ctrl click: dragging over // already-selected buildings must not toggle them off (REQ-UI-MULTI-SELECT). SelectionController controller; controller.selectBuildings({BuildingId(1), BuildingId(2)}, SelectionMode::Replace); controller.selectBuildings({BuildingId(2), BuildingId(3)}, SelectionMode::Add); REQUIRE(controller.getSelectedBuildings() == std::vector{BuildingId(1), BuildingId(2), BuildingId(3)}); } TEST_CASE("An additive field selection leaves the untouched category alone", "[selection]") { // Ctrl+clicking an actor passes an empty debris list, which must mean "do not // touch debris" rather than "clear debris" (REQ-UI-DEBRIS-MULTI-SELECT). SelectionController controller; controller.selectFieldObjects({}, {makeEntity(5)}, SelectionMode::Replace); controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Toggle); REQUIRE(controller.getSelectedActors() == std::vector{makeEntity(1)}); REQUIRE(controller.getSelectedDebris() == std::vector{makeEntity(5)}); } // --------------------------------------------------------------------------- // Membership queries used by the renderer // --------------------------------------------------------------------------- TEST_CASE("Membership queries answer per category", "[selection]") { SelectionController controller; controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)}, SelectionMode::Replace); REQUIRE(controller.isActorSelected(makeEntity(1))); REQUIRE_FALSE(controller.isActorSelected(makeEntity(2))); REQUIRE(controller.isDebrisSelected(makeEntity(2))); REQUIRE_FALSE(controller.isDebrisSelected(makeEntity(1))); } // --------------------------------------------------------------------------- // Published events // --------------------------------------------------------------------------- TEST_CASE("Selecting a building announces the categories it cleared", "[selection]") { SelectionController controller; controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)}, SelectionMode::Replace); SelectionEventSpy spy; controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace); REQUIRE(spy.buildingEvents == 1); REQUIRE(spy.actorEvents == 1); REQUIRE(spy.debrisEvents == 1); } TEST_CASE("Clearing an already-empty category announces nothing", "[selection]") { // The panels re-read on every event, so a redundant one is only noise — but the // point-click and box paths used to disagree about this, so it is pinned. SelectionController controller; SelectionEventSpy spy; controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace); REQUIRE(spy.buildingEvents == 1); REQUIRE(spy.actorEvents == 0); REQUIRE(spy.debrisEvents == 0); } TEST_CASE("Re-selecting the same building still announces it", "[selection]") { // Clicking an already-selected building refreshes its panel, so the event // fires even though the selection did not change. SelectionController controller; controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace); SelectionEventSpy spy; controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace); REQUIRE(spy.buildingEvents == 1); } TEST_CASE("Clearing an empty selection announces nothing at all", "[selection]") { SelectionController controller; SelectionEventSpy spy; controller.clearAll(); REQUIRE(spy.buildingEvents == 0); REQUIRE(spy.actorEvents == 0); REQUIRE(spy.debrisEvents == 0); } TEST_CASE("Clearing a populated selection announces every populated category", "[selection]") { SelectionController controller; controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)}, SelectionMode::Replace); SelectionEventSpy spy; controller.clearAll(); REQUIRE(spy.buildingEvents == 0); // buildings were already empty REQUIRE(spy.actorEvents == 1); REQUIRE(spy.debrisEvents == 1); REQUIRE(controller.getSelectedActors().empty()); REQUIRE(controller.getSelectedDebris().empty()); } // --------------------------------------------------------------------------- // Pruning despawned entities // --------------------------------------------------------------------------- TEST_CASE("Pruning announces only when something actually went", "[selection]") { // Runs every frame, so re-announcing an unchanged selection would spam the // panels 60 times a second. SelectionController controller; controller.selectFieldObjects({makeEntity(1), makeEntity(2)}, {}, SelectionMode::Replace); SelectionEventSpy spy; controller.setSelectedActors({makeEntity(1), makeEntity(2)}); REQUIRE(spy.actorEvents == 0); controller.setSelectedActors({makeEntity(1)}); REQUIRE(spy.actorEvents == 1); REQUIRE(controller.getSelectedActors() == std::vector{makeEntity(1)}); }