Files
dota_factory/src/test/SelectionControllerTest.cpp

283 lines
10 KiB
C++

#include "catch.hpp"
#include <memory>
#include <vector>
#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<SelectionChangedEvent,
EntitySelectionChangedEvent,
DebrisSelectionChangedEvent>
{
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<const SelectionChangedEvent> /*event*/) override
{
++buildingEvents;
}
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> /*event*/) override
{
++actorEvents;
}
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> /*event*/) override
{
++debrisEvents;
}
};
static entt::entity makeEntity(int index)
{
return static_cast<entt::entity>(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>{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<entt::entity>{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<entt::entity>{makeEntity(1)});
REQUIRE(controller.getSelectedDebris() == std::vector<entt::entity>{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<entt::entity>{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>{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>{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>{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>{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<entt::entity>{makeEntity(1)});
REQUIRE(controller.getSelectedDebris() == std::vector<entt::entity>{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<entt::entity>{makeEntity(1)});
}